为什么wait()方法必须在synchronized保护的同步代码中使用

注释中可以看到

The recommended approach to waiting is to check the condition being awaited in a {@code while} loop around the call to {@code wait}
The current thread must own this object’s monitor lock.

Lost Wake-Up Problem无法唤醒

什么意思

线程A调用object对象的 wait() 方法进入阻塞状态,接下来 没有其他线程 调用或者 其他线程都是提前调用 了object对象的 notify() 或者 notifyAll() 方法,这样线程A就会一直阻塞下去

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BlockingQueue {

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

public void give(String data) {
buffer.add(data);
notifyAll();
}

public String take() throws InterruptedException {
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();

}
}

对于上面消费者生产者的例子,当消费者线程进入了while代码段,但是调用 wait() 之前由于线程调度暂停了,生产者线程此时执行了整个 give() 方法,等到消费者线程恢复的时候,再调用 wait() ,就会一直等待。陷入无穷唤醒

这个现象的根本问题就是对于消费者线程来说, buffer.isEmpty()wait() 并不是原子操作,在中间被打断了,是线程不安全的。
所以说需要一个机制使得其他线程不会在判断-等待的间隙影响消费者线程

解决办法

随意加锁可行吗

定义一把锁 Lock lock = new Lock()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BlockingQueue {

Lock lock = new ReentrantLock();

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

public void give(String data) {
lock.lock();
buffer.add(data);
notifyAll();
lock.unlock();
}

public String take() throws InterruptedException {
lock.lock()
while (buffer.isEmpty()) {
wait();
}
String res = buffer.remove();
lock.unlock();
return res;
}
}

当消费者加锁进入,调用了 wait() 方法后并不会释放lock锁,然后生产者就会一直阻塞在第8行 lock.lock() ,注意,生产者线程阻塞在这里的时候其实是处于waiting状态

应该加什么锁?

我们需要一种机制能够让消费者调用wait的时候还能够释放其持有的锁,

只有BlockingQueue的对象锁才能够避免这个问题,也就是把give和take的操作放到synchronized同步块中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class BlockingQueue {

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

public void give(String data) {
synchronized(this){
buffer.add(data);
notifyAll();
}
}

public String take() throws InterruptedException {
synchronized(this){
while (buffer.isEmpty()) {
wait();
}
return buffer.remove();
}
}
}

上述例子中,消费者进入waiting状态后,生产者进入synchronized代码块,执行了notifyAll/notify方法后,并不会立马唤醒消费者线程(MESA管程),只是通知消费者线程可以去竞争管程锁了,然后自己继续执行

在调用 obj.wait() 前必须要拿到当前obj对象的管程monitor对象,也就是obj的锁,这样在调用 obj.wait() 的时候就可以释放obj锁。如果锁的不是正确的对象,那么Java就会抛出 IllegalMonitorStateException 异常。

wait方法为什么要在loop中

MESA管程

这是MESA管程独有的

1
2
3
4

while(条件不满足) {
wait();
}

Hasen 模型、Hoare 模型和 MESA 模型的一个核心区别就是当条件满足后,如何通知相关线程。
管程要求同一时刻只允许一个线程执行,那当线程T2的操作使线程T1等待的条件满足时,A和B究竟谁可以执行呢?

  • Hasen 模型,要求 notify() 放在代码的最后,这样 T2 通知完 T1 后,T2 就结束了,然后 T1 再执行,这样就能保证同一时刻只有一个线程执行。
  • Hoare 模型,T2 通知完 T1 后,T2 阻塞,T1 马上执行;等 T1 执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型,T2 多了一次阻塞唤醒操作。
  • MESA 管程,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,从入口队列进入到条件变量的等待队列,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量

虚假唤醒spurious wakeup

线程可能在没有被notify/notifyaAll也没有被中断或者超时的情况下被唤醒,这个概率非常小,但我们还是需要保证在发生虚假唤醒时候的正确性

notify和notifyAll方法什么时候使用

除非经过深思熟虑,否则尽量使用 notifyAll()

synchronized是非公平锁,
当我们使用 notifyAll() 时,它将唤醒所有的 wait 队列线程争夺锁,假如线程一个线程获取到了synchronized锁,但是不满足while的条件再次进入wait 时,其他唤醒的 wait 队列的线程仍旧会去争夺锁

想要使用notify需要满足下面三个条件

  • 所有等待线程拥有相同的等待条件;
  • 所有等待线程被唤醒后,执行相同的操作;
  • 只需要唤醒一个线程。

主要还是为了避免发生死等现象

为什么wait/notify/notifyAll在Object中,而sleep在Thread类中

wait/notify/notifyAll是锁级别的操作,管程monitor是对象级别的,所以在Object类中最合适

一个线程可能持有多把锁,如果wait方法在线程中,那么如何让一个线程持有多把锁呢,如何明确线程等待的是哪把锁呢?既然是让线程去等待某个对象的锁,所以自然应该通过操作对象来实现而不是操作线程

sleep和wait方法的异同

  • 都让线程进入waiting或者timed waiting方法中

  • 都可以响应interrupt中断,抛出InterruptedException

  • wait会主动释放monitor锁,sleep不会释放monitor锁

  • sleep必须定义一个超时时间

  • wait必须在拿到对象的管程锁后才能使用,wait需要放在loop里面

    Reference

https://juejin.cn/post/6939685932207439909
https://juejin.cn/post/6844904105811378189
https://time.geekbang.org/column/article/86089