为什么 wait 必须在 synchronized 保护的同步代码中使用?
在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的 while 代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须先持有对象的 monitor 锁,也就是通常所说的 synchronized 锁。
synchronized (obj) {
while (condition does not hold) {
obj.wait();
}
... // Perform action appropriate to condition
}
那么设计成这样有什么好处呢?
先看一段代码:
// 典型的生产者消费者的思想
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void add(String data) {
buffer.add(data);
notify();
}
public String remove() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
这段代码的问题在哪里呢?这段代码并没有受 synchronized 保护,于是便有可能发生以下场景:
1,首先,消费者线程调用 remove 方法并判断 buffer.isEmpty 方法是否返回 true,若为 true 代表 buffer 是空的,则消费者线程进入等待,但是在消费者线程调用 wait 方法之前,就被调度器暂停了,所以此时还没来得及执行 wait 方法。
2,此时生产者线程开始运行,执行了 add 方法,它往 buffer 中添加了数据,并执行了 notify 方法,但 notify 并没有任何效果,因为消费者线程的 wait 方法没来得及执行,所以没有线程在等待被唤醒。
3,此时,刚才被调度器暂停的消费者线程回来继续执行 wait 方法并进入了等待。
虽然刚才消费者判断了 buffer.isEmpty 条件,但真正执行 wait 方法时,之前的 buffer.isEmpty 的结果已经过期了,不再符合最新的场景了,因为这里的 判断-执行 不是一个原子操作,它在中间被打断了,是线程不安全的。假设这时没有更多的生产者进行生产,消费者便有可能陷入无穷无尽的等待,因为它错过了刚才 add 方法内的 notify 的唤醒。
我们看到正是因为 wait 方法所在的 remove() 方法没有被 synchronized 保护,所以它的 while 判断和 wait 方法无法构成原子操作,那么此时整个程序就很容易出错。
关键点:不是一个原子操作,它在中间被打断了,是线程不安全的。
Fix 一下上面的代码,这样写就对了:
public void add(String data) {
synchronized (this) {
buffer.add(data);
notify();
}
}
public String remove() throws InterruptedException {
synchronized (this) {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
另外,wait 方法会释放 monitor 锁,这也要求我们必须首先进入到 synchronized 内持有这把锁。
再来看第二个问题,为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
主要有两点原因:
1,Java 中每个对象都有一把称之为 monitor 监视器的锁,每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。
2,如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程在持有多把锁的情况下,如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。
再来看第三个问题,wait/notify 和 sleep 方法的异同?
我们先说相同点:
1,它们都可以让线程阻塞。
2,它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。
但是它们也有很多的不同点:
1,wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。
2,在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。
3,sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
4,wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
补充几点:
public final native void notify();
public final native void notifyAll();
public final void wait() throws InterruptedException {
wait(0);
}
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
// 此处对于纳秒的处理不精准,只是简单增加了1毫秒,
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
其中有 3 个方法是 native 的,也就是由虚拟机本地的 c 代码执行的。
wait()
方法:wait()
是要释放对象锁,进入等待池。既然是释放对象锁,那么肯定是先要获得锁。所以 wait()
必须要写在 synchronized 代
码块中,否则会报异常。
notify()
方法:也需要写在 synchronized
代码块中,调用对象的 notify()
,notifyAll()
这两个方法也需要先获得该对象的锁,然后去唤醒等待该对象锁的线程。notify 唤醒 等待池 中的一个线程,将这个线程放入该对象的锁池中。对象的锁池中的线程可以去竞争得到 对象锁
,然后开始执行。
关键词:等待池,对象的锁池。
如果是通过
notify()
唤起的线程,那先进入wait()
的线程会先被唤起来,并非随机唤醒;notifyAll()
则是唤起所有等待的线程。
假设有两个线程,线程 A 执行 wait()
方法时,线程 A 会被挂起,挂起意味着 wait()
后面的代码得不到执行。
当线程 B 执行 notify()
方法时,会唤醒被挂起的线程A,这个时候等待 B 线程执行完它所在方法之后,线程 A 被唤醒了,线程 A 才可以继续执行 wait()
方法后面的代码。
@Test
fun test() {
val lock = Object()
Thread {
synchronized(lock) {
try {
println("${Thread.currentThread().name} get lock")
TimeUnit.SECONDS.sleep(1)
lock.wait()
println("${Thread.currentThread().name} waked up")
} catch (ex: InterruptedException) {
ex.printStackTrace()
}
}
}.apply {
name = "thread-A"
}.start()
Thread {
synchronized(lock) {
println("${Thread.currentThread().name} get lock")
try {
TimeUnit.SECONDS.sleep(5)
} catch (ex: InterruptedException) {
ex.printStackTrace()
}
lock.notify()
println("${Thread.currentThread().name} notify() thread-A")
}
}.apply {
name = "thread-B"
}.start()
}