为什么wait()和notify()或notifyAll()需要搭配synchronized关键字使用

评论区有读者反馈文章内容难懂, 于是特意在文章开始前加一小段代码,辅助不熟悉 wait 和 notify 的读者理解。

请预测一下下面一段程序在 10 秒之内 的输出结果是什么,如果判断正确的话, 那么后续的内容介绍应该不会令你感到困惑

package com;

public class SyncThread implements Runnable {
    private static int count;
    private static String lock = "lock";
    public SyncThread() {
        count = 0;
    }

    public void run() {
        synchronized (lock) {
            System.out.printf( Thread.currentThread().getName() + "enter ");
            try {
                lock.wait(10*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        SyncThread syncThread = new SyncThread();
        Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
        Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
        thread1.start();
        thread2.start();
    }
}

答案是:

SyncThread1 enter 
SyncThread2 enter 

先修知识:

  • synchronized 的含义:

    • Java 中每一个对象都可以成为一个监视器(Monitor), 该 Monitor 由一个锁(lock), 一个等待队列(waiting queue ), 一个入口队列 (entry queue).
    • 对于一个对象的方法, 如果没有synchronized关键字, 该方法可以被任意数量的线程,在任意时刻调用。
    • 对于添加了synchronized关键字的方法,任意时刻只能被唯一的一个获得了对象实例锁的线程调用。
    • synchronized用于实现多线程的同步操作
  • wait()功用

    • wait(), notify(), notifyAll()synchonized 需要搭配使用, 用于线程同步
    • wait() 总是在一个循环中被调用,挂起当前线程来等待一个条件的成立。 Wait 调用会一直等到其他线程调用notifyAll()时才返回。
    • 当一个线程在执行synchronized 的方法内部,调用了wait()后, 该线程会释放该对象的锁, 然后该线程会被添加到该对象的等待队列中(waiting queue), 只要该线程在等待队列中, 就会一直处于闲置状态, 不会被调度执行。 要注意wait()方法会强迫线程先进行释放锁操作,所以在调用wait()时, 该线程必须已经获得锁,否则会抛出异常。由于wait()在 synchonized 的方法内部被执行, 锁一定已经获得, 就不会抛出异常了。
  • notify()的功用

    • wait(), notify(), notifyAll()synchonized 需要搭配使用, 用于线程同步
    • 当一个线程调用一个对象的notify()方法时, 调度器会从所有处于该对象等待队列(waiting queue)的线程中取出任意一个线程, 将其添加到入口队列 (entry queue) 中. 然后在入口队列中的多个线程就会竞争对象的锁, 得到锁的线程就可以继续执行。 如果等待队列中(waiting queue)没有线程, notify()方法不会产生任何作用
    • notifyAll()notify()工作机制一样, 区别在于notifyAll()会将等待队列(waiting queue)中所有的线程都添加到入口队列中(entry queue)
    • 注意, notifyAll()notify()更加常用, 因为notify()方法只会唤起一个线程, 且无法指定唤醒哪一个线程,所以只有在多个执行相同任务的线程在并发运行时, 我们不关心哪一个线程被唤醒时,才会使用notify()

为什么wait()notify()notifyAll()需要搭配synchronized关键字使用

  • 从语义角度来讲, 一个线程调用了wait()之后, 必然需要由另外一个线程调用notify()来唤醒该线程, 所以本质上, wait()notify()的成对使用, 是一种线程间的通信手段。
  • 进一步分析, wait() 操作的调用必然是在等待某种条件的成立, 而条件的成立必然是由其他的线程来完成的。 所以实际上, 我们调用 wait() 的时候, 实际上希望达到如下的效果
// 线程A 的代码
while(!condition){ // 不能使用 if , 因为存在一些特殊情况, 使得线程没有收到 notify 时也能退出等待状态
    wait();
}
// do something
// 线程 B 的代码
if(!condition){ 
    // do something ...
    condition = true;
    notify();
}
  • 现在考虑, 如果wait()notify() 的操作没有相应的同步机制, 则会发生如下情况
  1. 【线程 A】 进入了 while 循环后(通过了 !condition 判断条件, 但尚未执行 wait 方法), CPU 时间片耗尽, CPU 开始执行线程 B 的代码
  2. 【线程 B】 执行完毕了 condition = true; notify(); 的操作, 此时【线程 A】的 wait() 操作尚未被执行, notify() 操作没有产生任何效果
  3. 【线程 A】执行wait() 操作, 进入等待状态,如果没有额外的 notify() 操作, 该线程将持续在 condition = true 的情形下, 持续处于等待状态得不到执行。

由此看出, 在使用 wait() 和 notify() 这种会挂起线程的操作时, 我们需要一种同步机制保证, condition 的检查与 wait() 操作, 以及 condition 的更新与 notify() 是互斥的。

  • 那是否简单的将之前的代码包裹在一个 synchronized 代码块中就可以满足需求呢? 像下面这样。
// 线程A 的代码
synchronized(obj_A)
{
    while(!condition){ 
        wait();
    }
    // do something 
}
// 线程 B 的代码
synchronized(obj_A)
{
    if(!condition){ 
        // do something ...
        condition = true;
        notify();
    }
}
  • 乍一看, 上述的代码可以解决问题, 但是仔细分析一下, 由于wait() 操作会挂起当前线程, 那么必然需要在挂起前释放掉 obj_A 的锁, 但如果 obj_A 允许是任意对象, wait() 函数作为一个没有参数输入的方法,无从得知应该释放哪个对象的锁 。于是很自然的, 语法就会被设计成 java 现在的样子。即基于对象的 wait()notify() 的调用, 必须先获得该对象的锁。

  • 正确的用法示例如下

// 线程 A 的代码
synchronized(obj_A)
{
    while(!condition){ 
        obj_A.wait();
    }
    // do something 
}
// 线程 B 的代码
synchronized(obj_A)
{
    if(!condition){ 
        // do something ...
        condition = true;
        obj_A.notify();
    }
}

马士兵多线程例子:

/**
 * 经典面试题:写一个固定容量的容器,拥有put和get方法,以及getCount方法
 * 能够支持2个生产者线程以及10个消费者线程的阻塞调用
 * 
 * 点:生产者消费者模式
 * 
 * 如果调用 get方法时,容器为空,get方法就需要阻塞等待
 * 如果调用 put方法时,容器满了,put方法就需要阻塞等待
 * 
 * 实现方式:
 * 1. wait/notify
 * 2. Condition
 */
public class MyContainer1<T> {
    
    private final LinkedList<T> list = new LinkedList<>();
    private final int MAX = 10;
    private int count = 0;

    public synchronized void put(T t) {
        while (MAX == count) { // 如果容量最大,释放锁等待    ///【这里为什么使用while,而不是使用if???】
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 否则 put 
        list.add(t);
        ++count;
        this.notifyAll(); // 通知消费者线程,可以消费了
        // 【这里为什么调用 notifyAll 而不是 notify ?】
    }

    public synchronized T get() {
        while (list.size() == 0) { // 如果容量为空,释放锁等待  
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 否则获取
        T t = list.removeFirst();
        count--;
        this.notifyAll(); // 通知生产者线程生产
        return t;
    }

    public static void main(String[] args) {
        MyContainer1<String> c = new MyContainer1<>();
        // 启动消费者线程
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    System.out.println("c "+c.get());
                }
            }, "c_" + i ).start();
        }

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 2; i++) {
            new Thread(()->{
                for (int j = 0; j < 25; j++) {
                    c.put(Thread.currentThread().getName() + " " + j);
                }
            }, "p_" + i).start();
        }
    }
}

为什么使用while 而不是使用 if ???
在与wait()的配合中,百分之99的程序都是与while而不是if结合使用。
上述代码中,在容器已满的情况下,put方法会wait等待,当容器中的元素被消费者消费了一部分,就会唤醒所有put方法,
put方法会继续向下执行,直接执行list.add(t),生产者执行一次add,让出CPU以后,下一个时间片也可能给了生产者线程,那么多个生产者线程执行list.add() 就有可能出现数据一致性的问题。
如果使用while则会循环判断,就避免了这些问题。
另外,如果用if,生产者执行一次add,然后notifyAll,此时,生产者,消费者都在monitor的waitset中,可能会唤醒其他的生产者,这种情况下,生产者不判断,直接从wait以后的代码执行,则
会使list超额。(如果使用lock的condition,可以明确唤醒哪个,但是这里的notify不行)
不是有锁吗?为什么会需要循环判断?
wait之后,锁就会失去,再次被唤醒时,并且得到锁之后,是从list.add()开始执行的,会无判断直接加入到容器中。

为什么调用 notifyAll 而不是 notify ?
因为notify有可能再次叫醒一个生产者线程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容