Java Concurrency - wait & notify, 等待通知机制
生产者消费者问题是一个常见的多线程同步案例:一组生产者线程和一组消费者线程共享一个初始状态为空、大小为 N 的缓冲区。只有当缓冲区没满的时候,生产者才能把消息放入缓冲区,否则必须等待;只有缓冲区不空的时候,消费者才能从缓冲区取消息,否则必须等待。由于缓冲区是临界资源,在同一时间,它只允许一个生产者放入消息,或者一个消费者从中取出消息。
生产者消费者问题涉及到了多个线程通信的等待通知机制,生产者线程和消费者只能满足一定条件下才能操作共享的缓冲区,否则只能挂起等待,直到被唤醒重新竞争共享资源。Java API 提供了几个方法来实现这一机制:wait、notify、notifyAll。
wait: 在调用 wait 方法之前,线程必须获得该对象的对象级别锁。即只能在同步方法或同步块中调用 wait 方法,否则 JVM 会抛出 IllegalMonitorStateException 异常。在执行 wait 方法后,当前线程会释放锁。在 wait 方法返回之前,线程和其他线程重新竞争该对象锁。
notify: 在调用 notify 方法之前,线程同样须要获得该对象的对象级别锁,否则 JVM 会抛出 IllegalMonitorStateException 异常。notify 方法通知那些呈 wait 态的其他线程,如果有多个线程等待,则选择其中一个线程准备获得该对象锁。notify 方法执行完毕,呈 wait 状态的线程并不会立即获得该对象锁,须等到执行 notify 方法的线程执行完同步方法或同步块后,对象锁才会被释放,呈 wait 状态的线程才能获得锁。
notifyAll: notifyAll 与 notify 在于,notify 只唤醒一个等待锁的线程,而 notifyAll 会唤醒所有等待锁的线程。
生产者消费者问题示例
临界资源缓冲区
import java.util.ArrayList; import java.util.List; /** * 仓库 * @author huey * @created 2016年10月19日 * @updated 2016年10月19日 * @version 1.0.0 */ public class Storehouse { private List<Object> buffer; private int maxSize; // 仓库最多允许存储 maxSize 个物品 public Storehouse(int maxSize) { if (maxSize <= 0 || maxSize > 100) { throw new IllegalArgumentException("maxSize 参数的值必须在 1 到 100之间"); } this.maxSize = maxSize; buffer = new ArrayList<Object>(); } public synchronized void add(Object data) { if (buffer.size() == maxSize) { throw new IndexOutOfBoundsException("仓库已经饱和,不能再输入物品。"); } buffer.add(data); System.out.printf("输入物品,仓库当前有 %d 个物品。\n", buffer.size()); } public synchronized void remove() { buffer.remove(0); System.out.printf("输出物品,仓库当前有 %d 个物品。\n", buffer.size()); } public boolean isEmpty() { return buffer.isEmpty(); } public boolean isFull() { return buffer.size() == maxSize; } }
生产者
import java.util.Random; /** * 生产者 * @author huey * @created 2016年10月19日 * @updated 2016年10月19日 * @version 1.0.0 */ public class Producer extends Thread { private Storehouse storehouse; public Producer(String name, Storehouse storehouse) { super(name); this.storehouse = storehouse; } /** * 生产者生产物品 * @author huey * @created 2016年10月19日 * @updated 2016年10月19日 */ public void produce() { synchronized (storehouse) { while (storehouse.isFull()) { try { System.out.printf("仓库当前处于饱和状态,生产者 %s 须挂起等待。\n", Thread.currentThread().getName()); storehouse.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } storehouse.add(new Object()); storehouse.notifyAll(); } } @Override public void run() { for (int i = 0; i < 10; i++) { this.produce(); try { Thread.sleep(new Random().nextInt(50)); } catch (InterruptedException e) { e.printStackTrace(); } } } }
消费者
import java.util.Random; /** * 消费者 * @author huey * @created 2016年10月19日 * @updated 2016年10月19日 * @version 1.0.0 */ public class Consumer extends Thread { private Storehouse storehouse; public Consumer(String name, Storehouse storehouse) { super(name); this.storehouse = storehouse; } /** * 消费者消费物品 * @author huey * @created 2016年10月18日 * @updated 2016年10月18日 */ public void consume() { synchronized (storehouse) { while (storehouse.isEmpty()) { try { System.out.printf("仓库当前处于空虚状态,消费者 %s 须挂起等待。\n", Thread.currentThread().getName()); storehouse.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } storehouse.remove(); storehouse.notifyAll(); } } @Override public void run() { for (int i = 0; i < 5; i++) { this.consume(); try { Thread.sleep(new Random().nextInt(100)); } catch (InterruptedException e) { e.printStackTrace(); } } } }
测试用例
/** * * @author huey * @created 2016年10月19日 * @updated 2016年10月19日 * @version 1.0.0 */ public class Main { public static void main(String[] args) { Storehouse storehouse = new Storehouse(5); Thread[] producers = new Thread[] { new Producer("P1", storehouse), new Producer("P2", storehouse) }; for (Thread producer : producers) { producer.start(); } Thread[] consumers = new Thread[] { new Consumer("C1", storehouse), new Consumer("C2", storehouse), new Consumer("C3", storehouse), new Consumer("C4", storehouse) }; for (Thread consumer : consumers) { consumer.start(); } } }
运行结果:
输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 仓库当前处于空虚状态,消费者 C2 须挂起等待。 仓库当前处于空虚状态,消费者 C1 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C2 须挂起等待。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 仓库当前处于空虚状态,消费者 C3 须挂起等待。 仓库当前处于空虚状态,消费者 C1 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C3 须挂起等待。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 仓库当前处于空虚状态,消费者 C2 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 仓库当前处于空虚状态,消费者 C3 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C3 须挂起等待。 仓库当前处于空虚状态,消费者 C2 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C3 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 输入物品,仓库当前有 1 个物品。 输入物品,仓库当前有 2 个物品。 输出物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C2 须挂起等待。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C2 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 仓库当前处于空虚状态,消费者 C3 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 仓库当前处于空虚状态,消费者 C1 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C4 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C1 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。 仓库当前处于空虚状态,消费者 C3 须挂起等待。 输入物品,仓库当前有 1 个物品。 输出物品,仓库当前有 0 个物品。