工作三年,小胖连 wait/notify/notifyAll 都不会用?真的菜!

前几篇复习了下线程的创建方式、线程的状态、Thread 的源码这几篇文章,这篇讲讲 Object 几个跟线程获取释放锁相关的方法:wait、notify、notifyAll。

wait 方法源码解析

由于 wait() 是 Object 类的 native 方法,在 idea 中,它长这样:

public final native void wait(long timeout) throws InterruptedException;

看不了源码,那只能看源码的注释,注释太长,我摘取一些关键的点出来:

1、
Causes the current thread to wait until either another thread 
invokes the notify() method or the notifyAll() method for this object, 
or a specified amount of time has elapsed.

The current thread must own this object's monitor.

2、
In other words,waits should always occur in loops.like this one:
synchronized(obj) {
    while (condition does not hold)
        obj.wait(timeout);
    // Perform action appropriate to condition
}

3、
@throws IllegalArgumentException
if the value of timeout isnegative.

@throws IllegalMonitorStateException
if the current thread is notthe owner of the object 's monitor.

@throws InterruptedException
if any thread interrupted the current thread before or while the current thread was waiting
for a notification.
The interrupted status of the current thread is cleared when this exception is thrown.

注释中提到几点:

  • wait 会让当前线程进入等待状态,除非其他线程调用了 notify 或者 notifyAll 方法唤醒它,又或者等待时间到。另外,当前线程必须持有对象监控器(也就是使用 synchronized 加锁)

  • 必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 加锁。

  • 超时时间非法,抛 IllegalArgumentException 异常;不持有对象的 monitor 锁,抛 IllegalMonitorStateException 异常;在等待期间被其他线程中断,抛出 InterruptedException 异常。

为什么 wait 必须在 synchronized 保护的同步代码中使用?

逆向思考下,没有 synchronized 保护的情况下,我们使用会出现啥问题?先试着来模拟一个简单的生产者消费者例子:

public class BlockingQueue {

    Queue<String> buffer = new LinkedList<String>();

    // 生产者,负责往队列放数据
    public void give(String data) {
        buffer.add(data);
        notify();
    }

    // 消费者,主要是取数据
    public String take() throws InterruptedException {
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }

}

首先 give () 往队列里放数据,放完以后执行 notify 方法来唤醒之前等待的线程;take () 检查整个 buffer 是否为空,如果为空就进入等待,如果不为空就取出一个数据。但在这里我们并没有用 synchronized 修饰。假设我们现在只有一个生产者和一个消费者,那就有可能出现以下情况:

  • 此时,生产者无数据。消费者线程调用 take(),while 条件为 true。正常来说,这时应该去调用 wait() 等待,但此时消费者在调用 wait 之前,被被调度器暂停了,还没来得及调用 wait。

  • 到生产者调用 give 方法,放入数据并视图唤醒消费者线程。可这个时候唤醒不起作用呀。消费者并没有在等待。

  • 最后,消费者回去调用 wait 方法,就进入了无限等待中。

看明白了吗?第一步时,消费者判断了 while 条件,但真正执行 wait 方法时,生产者已放入数据,之前的 buffer.isEmpty 的结果已经过期了,因为这里的 “判断 - 执行” 不是一个原子操作,它在中间被打断了,是线程不安全的

正确的写法应该是这样子的:以下写法就确保永远 notify 方法不会在 buffer.isEmpty 和 wait 方法之间被调用,也就不会有线程安全问题。

public class BlockingQueue {

    Queue<String> buffer = new LinkedList<String>();

    // 生产者,负责往队列放数据
    public void give(String data) {
        synchronized(this) {
            buffer.add(data);
            notify();
        }
    }

    // 消费者,主要是取数据
    public String take() throws InterruptedException {
        synchronized(this) {
            while (buffer.isEmpty()) {
                wait();
            }
            return buffer.remove();
        }
    }

}

notify & notifyAll

notify & notifyAll 都是 Object 的 native 方法,在 IDEA 中看不到它的源码,同样是只能看注释。

public final native void notify();

public final native void notifyAll();

注释中主要提到以下几点:

  • notify() 随机唤醒一个等待该对象锁的线程,即使是多个也随机唤醒其中一个(跟线程优先级无关)。
  • notifyAll() 通知所有在等待该竞争资源的线程,谁抢到锁谁拥有执行权(跟线程优先级无关)。
  • 当前线程不持有对象的 monitor 锁,抛 IllegalMonitorStateException 异常。

为啥 wait & notify & notifyAll 定义在 Object 中,而 sleep 定义在 Thread 中?

两点原因:

  • Java 的每个对象都有一把称之为 monitor 监视器的锁,每个对象都可以上锁,所以在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而不是线程级别的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的 wait 方法就有意义了,它等待的就是这个对象的锁。如果 wait 方法定义在 Thread 类中,线程正在等待的是哪个锁就不明显了。简单来说,由于 wait & notify & notifyAll 是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。

  • 再者,如果把它们定义在 Thread 中,会带来很多问题。一个线程可以有多把锁,你调用 wait 或者 notify,我怎么知道你要等待的是哪把锁?唤醒的哪个线程呢?

wait 和 sleep 的异同

上次的文章我们已经看过了 sleep 的源码了,它们的相同点主要有:

  • 它们都可以改变线程状态,让其进入计时等待。
  • 它们都可以响应 interrupt 中断,并抛出 InterruptedException 异常。

不同点:

  • wait 是 Object 类的方法,而 sleep 是 Thread 类的方法。
  • wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法可在任意地方。
  • 调用 sleep 方法不释放 monitor 锁,调用 wait 方法,会释放 monitor 锁。
  • sleep 时间一到马上恢复执行(因为没有释放锁);wait 需要等中断,或者对应对象的 notify 或 notifyAll 才会恢复,抢到锁才会执行(唤醒多个的情况)。

小福利

如果看到这里,喜欢这篇文章的话,请帮点个好看。微信搜索一个优秀的废人,关注后回复电子书送你 100+ 本编程电子书 ,不只 Java 哦,详情看下图。回复 1024送你一套完整的 java 视频教程。

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

推荐阅读更多精彩内容