Java线程和并发 - Waiting and Notification
Java提供了线程间通信的API-一个线程可以等一个条件继续执行,在未来,另一个线程会增加该条件,然后提醒等待中的线程。。
Wait-and-Notify API
Object类提供了Wait-and-Notify API,由三个wait()方法、一个notify()方法还有一个notifyAll()方法组成。wait()方法等待一个ie条件,notify()和notifyAll()方法提醒等待中的线程:
- void wait():导致当前线程等待,直到另一个线程调用了该对象的notify()方法或者notifyAll()方法。当前线程在等待中的时候,可以被其他线程中断
- void wait(long timeout):当前线程最多等timeout毫秒,如果timeout是负的,抛IllegalArgumentException异常
- void wait(long timeout, int nanos):当前线程最多等待timeout毫秒和nanos纳秒。如果timeout或者nanos是负的,或者nanos大于999999,抛IllegalArgumentException
- void notify():唤醒一个等待该对象的monitor的线程。如果有任何线程等待该对象,其中一个被唤醒。选择是任意的,由实现决定。当前线程解锁以后,被唤醒的线程才可以继续执行
- void notifyAll():唤醒等待该对象的monitor的所有线程
该API利用一个对象的条件队列,等待中的线程也叫wait set。因为条件队列紧紧地绑定到一个对象的锁,所有的五个方法必须在一个同步的环境下被调用(当前线程必须是对象monitor的所有者);否则,抛IllegalMonitorStateException。
下面的伪代码演示了无参数的wait()方法:
synchronized(obj) {
while (<condition does not hold>)
obj.wait();
// 执行适合条件的操作
}
这样,在一个synchronized块内调用wait()方法,因为可能伪唤醒(线程醒来,但是没有被notified、中断和超时),所以当条件不满足,就一直等待。循环退出后,开始执行符合条件的动作。
警告:不要在循环体外调用wait()方法。循环在wait()调用之前和之后测试条件。在之前测试确保还是活的。如果不做测试,而且在调用wait()之前调用了notify(),等待的线程可能不会被唤醒。在调用wait()之后做测试可以确保安全。如果不重做测试,而且线程被唤醒后条件不成立(比如其他线程调用了notify()),线程会销毁手锁保护的不变性。
对应的notify()方法可能是这样调用的:
synchronized(obj) {
// 设置条件
obj.notify();
}
使用相同的对象引用调用notify()方法。
Difference between notify() and notifyAll()
有更多的讨论。如果你想知道该用哪个方法,如果只有两个相关线程,我会使用notify(),其中一个线程偶尔会等待另一个线程的通知。否则,我使用notifyAll()。
生产者和消费者
涉及conditions的线程间通信的经典例子是生产者线程和消费者线程。生产者线程生产由消费者线程消费的数据。每个数据保存在共享变量中。
设想线程运行速度不同的场景。生产的速度可能比消费的速度快,当然也有可能,消费者可能要等待数据的生成。要解决这些问题,生产者线程必须等待,直到它被提醒先前生产的数据被消费掉了,消费者也得等待新的数据被生产出来:
public class PC {
public static void main(String[] args) {
Shared s = new Shared();
new Producer(s).start();
new Consumer(s).start();
}
private static class Shared {
private char c;
private volatile boolean writeable = true;
synchronized void setSharedChar(char c) {
while (!writeable)
try {
wait();
} catch (InterruptedException ie) {
}
this.c = c;
writeable = false;
notify();
}
synchronized char getSharedChar() {
while (writeable)
try {
wait();
} catch (InterruptedException ie) {
}
writeable = true;
notify();
return c;
}
}
private static class Producer extends Thread {
private final Shared s;
Producer(Shared s) {
this.s = s;
}
@Override
public void run() {
for (char ch = 'A'; ch <= 'Z'; ch++) {
s.setSharedChar(ch);
System.out.println(ch + " produced by producer.");
}
}
}
private static class Consumer extends Thread {
private final Shared s;
Consumer(Shared s) {
this.s = s;
}
@Override
public void run() {
char ch;
do {
ch = s.getSharedChar();
System.out.println(ch + " consumed by consumer.");
} while (ch != 'Z');
}
}
}
上面的程序增加了一个Shared对象和两个拥有该对象的引用的线程。生产者调用setSharedChar()方法保存23个大写字母,消费者调用getSharedChar()方法获取每个字母。
writeable实例成员跟踪两个条件:生产者等待消费数据和消费者等待生产数据。它负责协调生产者和消费者。程序是这样执行的:
- 消费者执行s.getSharedChar()来检索字母
- 在同步方法内,因为writeable是true,消费者调用wait()。消费者等待数据生成的提醒
- 生产者终于执行了setSharedChar方法
- 当生产者进入同步方法的时候(这是有可能的,因为消费者在wait()方法内释放锁),发现writeable是true,不能调用wait()
- 生产者保存字母,设置writeable为false(这会导致在消费者还没消费该字母的时候,生产者等待下一次setSharedChar方法调用),并且调用notify()提醒消费者(假设消费者在等待)
- 生产者退出setSharedChar方法
- 消费者醒来,设置writeable为true(这会导致生产者没有生产出字母前,消费者等待下一次getSharedChar调用),提醒生产者唤醒消费者线程,返回共享的字母
你能获得类似这样的输出:
W produced by producer.
W consumed by consumer.
X produced by producer.
X consumed by consumer.
Y produced by producer.
Y consumed by consumer.
Z produced by producer.
Z consumed by consumer.
虽然同步工作正常,但是你可能在多个消费消息前,观察到多个生产消息:
A produced by producer.
B produced by producer.
A consumed by consumer.
B consumed by consumer.
甚至可能看到消费消息在生产消息之前:
Q consumed by consumer.
Q produced by producer.
奇怪的输出顺序,不是说生产者和消费者线程没有同步。而是setSharedChar()调用后面的System.out.println()没有同步,getSharedChar调用后面的println也没有同步。所以,可以这样修改程序:
public class PC {
public static void main(String[] args) {
Shared s = new Shared();
new Producer(s).start();
new Consumer(s).start();
}
private static class Shared {
private char c;
private volatile boolean writeable = true;
synchronized void setSharedChar(char c) {
while (!writeable)
try {
wait();
} catch (InterruptedException ie) {
}
this.c = c;
writeable = false;
notify();
}
synchronized char getSharedChar() {
while (writeable)
try {
wait();
} catch (InterruptedException ie) {
}
writeable = true;
notify();
return c;
}
}
private static class Producer extends Thread {
private final Shared s;
Producer(Shared s) {
this.s = s;
}
@Override
public void run() {
for (char ch = 'A'; ch <= 'Z'; ch++) {
synchronized (s) {
s.setSharedChar(ch);
System.out.println(ch + " produced by producer.");
}
}
}
}
private static class Consumer extends Thread {
private final Shared s;
Consumer(Shared s) {
this.s = s;
}
@Override
public void run() {
char ch;
do {
synchronized (s) {
ch = s.getSharedChar();
System.out.println(ch + " consumed by consumer.");
}
} while (ch != 'Z');
}
}
}