Java并发编程实战
王宝令
资深架构师
立即订阅
14233 人已学习
课程目录
已完结 50 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 你为什么需要学习并发编程?
免费
学习攻略 (1讲)
学习攻略 | 如何才能学好并发编程?
第一部分:并发理论基础 (13讲)
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
第二部分:并发工具类 (14讲)
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
第三部分:并发设计模式 (10讲)
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
第四部分:案例分析 (4讲)
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
第五部分:其他并发模型 (4讲)
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
结束语 (1讲)
结束语 | 十年之后,初心依旧
用户故事 (2讲)
用户来信 | 真好,面试考到这些并发编程,我都答对了!
3 个用户来信 | 打开一个新的并发世界
Java并发编程实战
登录|注册

06 | 用“等待-通知”机制优化循环等待

王宝令 2019-03-12

由上一篇文章你应该已经知道,在破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待,核心代码如下:

// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
复制代码

如果 apply() 操作耗时非常短,而且并发冲突量也不大时,这个方案还挺不错的,因为这种场景下,循环上几次或者几十次就能一次性获取转出账户和转入账户了。但是如果 apply() 操作耗时长,或者并发冲突量大的时候,循环等待这种方案就不适用了,因为在这种场景下,可能要循环上万次才能获取到锁,太消耗 CPU 了。

其实在这种场景下,最好的方案应该是:如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入等待状态;当线程要求的条件(转出账本和转入账本同在文件架上)满足后,通知等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗 CPU 的问题。

那 Java 语言是否支持这种等待 - 通知机制呢?答案是:一定支持(毕竟占据排行榜第一那么久)。下面我们就来看看 Java 语言是如何支持等待 - 通知机制的。

完美的就医流程

在介绍 Java 语言如何支持等待 - 通知机制之前,我们先看一个现实世界里面的就医流程,因为它有着完善的等待 - 通知机制,所以对比就医流程,我们就能更好地理解和应用并发编程中的等待 - 通知机制。

© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java并发编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(135)

  • 姜戈
    wait与sleep区别在于:
    1. wait会释放所有锁而sleep不会释放锁资源.
    2. wait只能在同步方法和同步块中使用,而sleep任何地方都可以.
    3. wait无需捕捉异常,而sleep需要.

    两者相同点:都会让渡CPU执行时间,等待再次调度!
    2019-03-12
    2
    157
  • Home
    补充一下姜戈同学回答;1 sleep是Thread的方法,而wait是Object类的方法;2:sleep方法调用的时候必须指定时间
    2019-03-12
    85
  • crazypokerk
    wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
    sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。

    作者回复: 👍

    2019-03-12
    64
  • wang
    public class MyLock {
    // 测试转账的main方法
    public static void main(String[] args) throws InterruptedException {
        Account src = new Account(10000);
        Account target = new Account(10000);
        CountDownLatch countDownLatch = new CountDownLatch(9999);
        for (int i = 0; i < 9999; i++) {
            new Thread(()->{
                src.transactionToTarget(1,target);
            countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println("src="+src.getBanalce() );
        System.out.println("target="+target.getBanalce() );
    }
    static class Account{ //账户类
        public Account(Integer banalce) {
            this.banalce = banalce;
        }
        private Integer banalce;
        public void transactionToTarget(Integer money,Account target){//转账方法
            Allocator.getInstance().apply(this,target);
            this.banalce -= money;
            target.setBanalce(target.getBanalce()+money);
            Allocator.getInstance().release(this,target);
        }
        public Integer getBanalce() {
            return banalce;
        }
        public void setBanalce(Integer banalce) {
            this.banalce = banalce;
        }
    }
    static class Allocator { //单例锁类
        private Allocator(){}
        private List<Account> locks = new ArrayList<>();
        public synchronized void apply(Account src,Account tag){
            while (locks.contains(src)||locks.contains(tag)) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                }
            }
            locks.add(src);
            locks.add(tag);
        }
        public synchronized void release(Account src,Account tag){
            locks.remove(src);
            locks.remove(tag);
            this.notifyAll();
        }
        public static Allocator getInstance(){
            return AllocatorSingle.install;
        }
        static class AllocatorSingle{
            public static Allocator install = new Allocator();
        }
    }
    }

    作者回复: 高手高手,让我写也这不这样好👍👍👍👍👍

    2019-03-12
    7
    49
  • 蜡笔
    老师你不用在文章中贴出所有代码嘛,只贴出核心代码,然后把整个例子放在github上,文末的时候给出github的链接,水平基础一般的就可以去上面下载下来跑一跑调试加深印象理解,这样可以不老师😂
    2019-03-12
    29
  • 虎虎❤️
    困惑
    1. 对于从来没有获得过互斥锁的线程 所在的等待队列 和 因为wait() 释放锁而进入了等待队列,是否是同一个等待队列?也就是图中左侧和右侧的是否为同一个队列?
    2. notifyAll() 会发通知给等待队列中所有的线程吗?包括那些从未获得过互斥锁的线程吗?
    3. 因为wait()被阻塞,而又因为notify()重新被唤醒后,代码是接着在wait()之后执行,还是重新执行 apply 方法?

    作者回复: 不是一个队列
    只唤醒右侧的队列
    wait之后

    2019-03-14
    1
    26
  • andywang
    应该是!als.contains(from) || !als.contains(to)才wait()吧
    2019-03-12
    2
    16
  • 郑晨Cc
    王老师 ABCD 那个例子真没看懂 线程1释放锁为啥会通知线程4?1和3才是互斥的啊 2和4互斥 按我的理解 3和4 不应该是在同一个等待队列里啊 因为不是通一把锁(准确来时不是同样的两把锁)
    就着这个例子 我还有个关互斥锁的等待队列的问题 假设还是资源ABCD 线程5 获取AB 线程6获取CD 线程7试图获取AB 线程8试图获取BC 线程9试图获取CD 那线程 7,8,9 到底是不是在一个等待队列里面,
    JVM在实现 wait notify机制是时候到底存不存在真实的队列?

    作者回复: 都是this这一把锁: synchronized void apply(){}
    所以是一个等待队列

    就是500个线程,也是同一个等待队列,因为锁的都是this

    队列一定是存在的

    2019-03-12
    4
    14
  • 我是卖报小行家
    wait和sleep区别
    1:wait释放资源,sleep不释放资源
    2:wait需要被唤醒,sleep不需要
    3:wait需要获取到监视器,否则抛异常,sleep不需要
    4:wait是object顶级父类的方法,sleep则是Thread的方法

    作者回复: 全面👍

    2019-03-12
    13
  • 邋遢的流浪剑客
    wait会释放当前占有的锁,sleep不会释放锁
    2019-03-12
    11
  • 老杨同志
    点赞@姜戈 补充一下:wait与sleep区别在于:
    1. wait会释放所有锁而sleep不会释放锁资源.
    2. wait只能在同步方法和同步块中使用,而sleep任何地方都可以.
    3. wait无需捕捉异常,而sleep需要.(都抛出InterruptedException ,wait也需要捕获异常)
    4. wait()无参数需要唤醒,线程状态WAITING;wait(1000L);到时间自己醒过来或者到时间之前被其他线程唤醒,状态和sleep都是TIME_WAITING
    两者相同点:都会让渡CPU执行时间,等待再次调度!

    作者回复: 👍

    2019-03-12
    10
  • lau
    看评论也能学到很多干货
    2019-03-14
    8
  • kaizhi
    感觉老师讲解的节奏非常好,能把并发讲解的这么浅显易懂绝非一日之功。老师在用一种由浅入深,逐层深入的方法来讲解java并发的这些知识点,而不是一股脑的把所有的知识点都罗列出来,有助于我们的吸收,也能引发我们的进一步思考,譬如这节的wait方法,就是在改进上一节的while循环等待(上一节的while在评论区就引发了各路高手的强烈不满,哈哈),这样有助于我们理解当年java的开发者在设计wait方法时的出发点在哪里,另外也让我们理解了为什么wait,notify,notifyAll这些方法会作为Object类的方法。用现实生活做类比这一点也很赞,之前有艺术来源于生活而又高于生活,现在可以说技术来源于生活而又高于生活了,哈哈~

    作者回复: 感谢你这么懂我😂

    2019-03-15
    7
  • 陈志凯
    public class Allocator {
    private final List<Account> als=new LinkedList<Account>();
    // 一次性申请所有资源
    public synchronized void apply(Account from, Account to) {
    // 经典写法
    while (als.contains(from) || als.contains(to)) {
    try {
    System.out.println("等待用户 -> "+from.getId()+"_"+to.getId());
    wait();
    } catch (Exception e) {
    //notify + notifyAll 不会来这里
    System.out.println("异常用户 -> "+from.getId()+"_"+to.getId());
    e.printStackTrace();
    }
    }
    als.add(from);
    als.add(to);
    }
    // 归还资源
    public synchronized void free(Account from, Account to) {
    System.out.println("唤醒用户 -> "+from.getId()+"_"+to.getId());
    als.remove(from);
    als.remove(to);
    notifyAll();
    }
    }

    public class Account {
    // actr 应该为单例
    private final Allocator actr;
    //唯一账号
    private final long id;
    //余额
    private int balance;
    public Account(Allocator actr,long id,int balance){
    this.actr=actr;
    this.id=id;
    this.balance=balance;
    }
    // 转账
    public void transfer(Account target, int amt) {
    // 一次性申请转出账户和转入账户,直到成功
    actr.apply(this, target);
    try {
    //TODO 有了资源管理器,这里的synchronized锁就不需要了吧?!
    if (this.balance > amt) {
    this.balance -= amt;
    target.balance += amt;
    }
    //模拟数据库操作时间
    try {
    Thread.sleep(new Random().nextInt(2000));
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    } finally {
    actr.free(this, target);
    }
    }
    @Override
    public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + (int) (id ^ (id >>> 32));
    return result;
    }
    /**
    * 用于判断两个用户是否一致
    */
    @Override
    public boolean equals(Object obj) {
    if (this == obj)
    return true;
    if (obj == null)
    return false;
    if (getClass() != obj.getClass())
    return false;
    Account other = (Account) obj;
    if (id != other.id)
    return false;
    return true;
    }
    public long getId() {
    return id;
    }
    }
    老师,以上代码是我补的,有个疑问,以上有了Allocator管理器(见TODO部分),transfer方法的this跟target都不再需要加synchronized锁了吧?!

    作者回复: 如果只是这个例子就不需要了,
    送你俩字!优秀!!!!!

    2019-03-13
    7
  • 高源
    老师最好讲解每一章的时候配合完整的例子源代码,这样再加调试源代码,印象更深刻了

    作者回复: 大家水平不一样,有些高水平的可能只想看到核心的代码,我怕贴多了,有人说浪费流量。自己补上剩余代码也是个不错的提高机会吧😂

    2019-03-12
    7
  • 陈志凯
    强烈建议老师每个章节配上完整的demo,包括模拟多线程多个客户操作的代码,这样看效果才是最佳的,我们自己也能根据代码实际好好观察!

    作者回复: 对于水平高的,完整的代码没必要。对于水平低的,完整的代码只能增加惰性。我就很讨厌粘贴一些无关的代码😃

    2019-03-12
    1
    6
  • aksonic
    老师,我昨天问了你问题后,带着疑问又去学习了下,是不是文章中的左边和右边的两个队列应该改一改名字,不应该都叫等待队列,这样对新手很容易产生误解。如果左边的叫做同步队列,右边的叫做等待队列可能更好。左边的队列是用来争夺锁的,右边的队列是等待队列,是必须被notify的,当被notify之后,就会被放入左边的队列去争夺锁。老师,你觉得呢?

    作者回复: 你这个建议挺好,在管程里面,会重新讲这俩队列。现在就知道有俩等待队列就可以了

    2019-03-14
    1
    5
  • 约书亚
    置顶回答是不是有问题,wait一样需要捕获InterruptedException异常呀?

    作者回复: 你是对的

    2019-03-12
    5
  • San D Ji
    学习这几章以后,我一直有一个问题,Javaweb端在什么样的业务场景下需要多线程的技术实现?
    一直以为Javaweb端都是接收到一个请求服务器端开启一条线程独立作业,完了之后就返回一个应答。
    不知道老师能否回答一下我的疑问?

    作者回复: 比如你要做个数据库连接池,做个httpclient,做个rpc框架,用批处理处理上千万数据,一个简单的crud真的用不上

    2019-03-12
    4
  • ^_^
    老师,while(als.contains(from) || als.contains(to)) 这句对吗

    作者回复: 我确认了一下,应该是对的
    只要有一个,就说明曾经被分配过

    2019-03-12
    2
    4
收起评论
99+
返回
顶部