正确使用 wait/notify/notify方法以及源码解析
时间:2022-07-25
本文章向大家介绍正确使用 wait/notify/notify方法以及源码解析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
前几篇复习了下《线程的创建方式》、《线程的状态》、《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 才会恢复,抢到锁才会执行(唤醒多个的情况)。
巨人的肩膀
- https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=239
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法