在 Java 中,synchronized 解决了多线程的竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用 synchronized 加锁:

  1. class TaskQueue {
  2. Queue<String> queue = new LinkedList<>();
  3. public synchronized void addTask(String s) {
  4. this.queue.add(s);
  5. }
  6. }

但是 synchronized 并没有解决多线程协调的问题。比如:

  1. class TaskQueue {
  2. Queue<String> queue = new LinkedList<>();
  3. public synchronized void addTask(String s) {
  4. this.queue.add(s);
  5. }
  6. public synchronized String getTask() {
  7. while (queue.isEmpty()) {
  8. }
  9. return queue.remove();
  10. }
  11. }

可以看到,上述的 getTask() 内部逻辑:先判断是否为空。如果为空,就循环等待,直到另一个线程往队列中放入了一个任务,while() 循环退出,就可以返回队列的元素了。
但实际上 while() 循环永远不会退出。因为线程在执行 while() 循环时,已经在 getTask() 入口获取了 this 锁,其他线程根本无法调用 addTask(),因为 addTask() 执行条件也是获取 this 锁。
因此,当队列为空的时候,getTask() 就会陷入死循环。
为了解决这个问题,我们可以在当前的锁对象(this)上调用 wait() 来让该线程进入等待状态:

  1. public synchronized String getTask() {
  2. while (queue.isEmpty()) {
  3. this.wait(); // 释放 this 锁
  4. // 重新获取 this 锁
  5. }
  6. return queue.remove();
  7. }

注意: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 锁中等待的线程 }
  1. 在队列添加完任务之后,线程立刻对 `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 ts = new ArrayList(); for (int i = 0; i < 5; i++) { Thread t = new Thread() { public void run() { // 执行task: while (true) { try { String s = q.getTask(); System.out.println(“execute task: “ + s); } catch (InterruptedException e) { return; } } } }; t.start(); ts.add(t); } Thread add = new Thread(() -> { for (int i = 0; i < 10; i++) { // 放入task: String s = “t-“ + Math.random(); System.out.println(“add task: “ + s); q.addTask(s); try { Thread.sleep(100); } catch (InterruptedException e) { } } }); add.start(); add.join(); Thread.sleep(100); for (Thread t : ts) { t.interrupt(); } } }

class TaskQueue { Queue queue = new LinkedList<>();

  1. public synchronized void addTask(String s) {
  2. this.queue.add(s);
  3. this.notifyAll(); // 唤醒全部等待的线程
  4. }
  5. public synchronized String getTask() throws InterruptedException {
  6. while (queue.isEmpty()) { // 这里的 while 是因为,线程唤醒之后需要再次判断当前队列是否为空
  7. this.wait();
  8. }
  9. return queue.remove();
  10. }

}

  1. 该例子中的唤醒线程与上一个例子不同,用的是 `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(); }

  1. `if` 语句的写法是错误的。正如上面的例子,3 个线程只有一个拿到 `this` 锁,该线程执行 `queue.remove()` 可以获取到队列中的元素。然而使用 `if` ,另外两个线程拿到 `this` 锁后,就直接 `queue.remove()` 返回队列元素,但是此时队列中已经没有元素,会造成逻辑错误。所以要使用 `while` 循环来再次让两个线程进入等待状态:

while (queue.isEmpty()) { this.wait(); }

``` 所以,正确编写多线程代码是非常困难的,需要仔细考虑的条件非常多,任何一个地方考虑不周,都会导致多线程运行时不正常。

小结

waitnotify 用于多线程协调运行:

  • synchronized 内部可以调用 wait() 使线程进入等待状态;
  • 必须在已获得的锁对象上调用 wait() 方法;
  • synchronized 内部可以调用 notify()notifyAll() 唤醒其他等待线程;
  • 必须在已获得的锁对象上调用 notify()notifyAll() 方法;
  • 已唤醒的线程还需要重新获得锁后才能继续执行。