在 Java 中,synchronized
解决了多线程的竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用 synchronized
加锁:
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
}
但是 synchronized
并没有解决多线程协调的问题。比如:
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() {
while (queue.isEmpty()) {
}
return queue.remove();
}
}
可以看到,上述的 getTask()
内部逻辑:先判断是否为空。如果为空,就循环等待,直到另一个线程往队列中放入了一个任务,while()
循环退出,就可以返回队列的元素了。
但实际上 while()
循环永远不会退出。因为线程在执行 while()
循环时,已经在 getTask()
入口获取了 this
锁,其他线程根本无法调用 addTask()
,因为 addTask()
执行条件也是获取 this
锁。
因此,当队列为空的时候,getTask()
就会陷入死循环。
为了解决这个问题,我们可以在当前的锁对象(this
)上调用 wait()
来让该线程进入等待状态:
public synchronized String getTask() {
while (queue.isEmpty()) {
this.wait(); // 释放 this 锁
// 重新获取 this 锁
}
return queue.remove();
}
注意:
wait()
方法必须在当前获取的锁对象上调用,这里获取的是this
锁,因此调用this.wait()
。
wait()
方法的执行机制非常复杂。首先,它不是一个普通的 Java 方法,而是定义在 Object
类的一个 native
方法,也就是由 JVM 的 C 代码实现的。其次,必须在 synchronized
块中才能调用 wait()
方法,因为 wait()
方法调用时,会释放线程获得的锁,wait()
方法返回后,线程又会重新试图获得锁。
使用 wait()
来等待线程后,就需要让等待的线程被重新唤醒。在相同的锁对象上调用 notify()
就可以唤醒的线程,有两种唤醒线程的方法:
notify
: 随机唤醒一个该锁对象中的等待线程notifyAll
: 唤醒该锁对象中的全部等待线程 ``` public synchronized void addTask(String s) { this.queue.add(s); this.notify(); // 随机唤醒 this 锁中等待的线程 }
在队列添加完任务之后,线程立刻对 `this` 锁对象调用 `notify()` 方法,这个方法会随机唤醒一个正在 `this` 锁等待的线程(就是在 `getTask()` 中位于 `this.wait()` 的线程),从而使得等待线程从 `this.wait()` 方法返回。<br />在来看一个完整的例子:
import java.util.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
TaskQueue q = new TaskQueue();
List
class TaskQueue {
Queue
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll(); // 唤醒全部等待的线程
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) { // 这里的 while 是因为,线程唤醒之后需要再次判断当前队列是否为空
this.wait();
}
return queue.remove();
}
}
该例子中的唤醒线程与上一个例子不同,用的是 `this.notifyAll()` ,将会唤醒所有当前正在 `this` 锁等待的线程。通常来说,`notifyAll()` 更安全。有些时候,如果我们的代码逻辑考虑不周,用 `notify()` 会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。<br />`wait()` 有个小坑,当 `wait()` 结束后会重新获得 `this` 锁。假设有 3 个线程在 `wait()` 等待状态,等待执行 `addTask()` 的线程结束方法后,才能释放 `this` 锁,随后,这 3 个线程只有一个能获取到 `this` 锁,剩下两个应该继续等待。<br />所以我们在 `getTask()` 中使用的是循环调用 `wait()` ,而不是 `if` 语句:
public synchronized String getTask() throws InterruptedException { if (queue.isEmpty()) { this.wait(); } return queue.remove(); }
`if` 语句的写法是错误的。正如上面的例子,3 个线程只有一个拿到 `this` 锁,该线程执行 `queue.remove()` 可以获取到队列中的元素。然而使用 `if` ,另外两个线程拿到 `this` 锁后,就直接 `queue.remove()` 返回队列元素,但是此时队列中已经没有元素,会造成逻辑错误。所以要使用 `while` 循环来再次让两个线程进入等待状态:
while (queue.isEmpty()) { this.wait(); }
``` 所以,正确编写多线程代码是非常困难的,需要仔细考虑的条件非常多,任何一个地方考虑不周,都会导致多线程运行时不正常。
小结
wait
和 notify
用于多线程协调运行:
- 在
synchronized
内部可以调用wait()
使线程进入等待状态; - 必须在已获得的锁对象上调用
wait()
方法; - 在
synchronized
内部可以调用notify()
或notifyAll()
唤醒其他等待线程; - 必须在已获得的锁对象上调用
notify()
或notifyAll()
方法; - 已唤醒的线程还需要重新获得锁后才能继续执行。