线程通常需要协调它们的动作。最常见的协调方法是被守护的代码块。这样的代码块以在代码块执行之前,对一个总是为真的条件进行轮询。为了能正确完成它,需要遵循一系列的步骤。
假设,guardJoy是一个方法——它不能推进,直到一个共享变量joy被另一个线程设定。这样的方法,从理论上来讲,只是简单地循环,直到条件被满足,但这个循环是多余的,因为它在等待的时候,一直在运作。
public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy) {}
System.out.println("Joy has been achieved!");
}
一个更有效率的方法,是调用Object.wait方法来暂停当前线程。对wait方法的调用不会返回,直到另一个线程发起一个通知——一些特殊的事件已经出现——尽管也未必是当前线程正在等待的事件。
public synchronized void guardedJoy() {
// This guard only loops once for each special event, which may not
// be the event we're waiting for.
while(!joy) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved!");
}
注意:总是在一个循环内部调用wait方法来测试等待条件。不要假设中断就是你所等待的那个。
就像很多会暂停运行的其他方法一样,wait方法会抛出InterruptedException。在这个例子中,我们可以忽略那个异常——我们只关心joy的值。
为什么就是这个版本的guardedJoy被同步?假设d是我们用来调用wait的对象。当一个线程调用d.wait时,它必须持有d对象的内部锁,否则会报错。在一个同步方法中调用wait是获得内部锁的简单方式。
当wait方法被调用后,线程释放锁并且暂停运行。在未来的某个时刻,另一个线程会调用相同的锁来调用Object.notifyAll方法,来告知所有在等待该锁的线程,有重要的事情发生了:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
在线程释放锁之后的某个时间,第一个线程重新获得锁,并且从调用wait方法中恢复过来。
注意:还有另一个通知方法,notify,它会唤醒一个线程。notify不允许你指定具体唤醒哪个线程,它只有在大型并发应用中才有用——在这种情况下,程序会有大量的线程,都在做相同的事情。在这种情况下,你不关心具体唤醒的是哪个线程。
让我们使用守护线程块来创建一个生产者-消费者应用。这种应用会在两个线程之间共享数据——生产者,负责创造数据,和消费者,负责对数据做一些处理。这两个线程通过共享的对象来交流。协作是非常重要的:消费者线程必须要在生产者交付它之后才能取数据,而生产者必须要等到消费者把旧数据取走,才能生产新的数据。
在这个例子中,数据是一系列的文本消息,通过Drop类的对象进行共享。
public class Drop {
// Message sent from producer
// to consumer.
private String message;
// True if consumer should wait
// for producer to send message,
// false if producer should wait for
// consumer to retrieve message.
private boolean empty = true;
public synchronized String take() {
// Wait until message is
// available.
while (empty) {
try {
wait();
} catch (InterruptedException e) {}
}
// Toggle status.
empty = true;
// Notify producer that
// status has changed.
notifyAll();
return message;
}
public synchronized void put(String message) {
// Wait until message has
// been retrieved.
while (!empty) {
try {
wait();
} catch (InterruptedException e) {}
}
// Toggle status.
empty = false;
// Store message.
this.message = message;
// Notify consumer that status
// has changed.
notifyAll();
}
}
消费者线程,被定义为Producer,发送一系列相似的信息。"DONE"字符串意味着所有的消息都被发送。为了模拟真实世界的不可预测性,生产者线程以随机停止的节奏发送信息。
import java.util.Random;
public class Producer implements Runnable {
private Drop drop;
public Producer(Drop drop) {
this.drop = drop;
}
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
Random random = new Random();
for (int i = 0;
i < importantInfo.length;
i++) {
drop.put(importantInfo[i]);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
}
drop.put("DONE");
}
}
消费者线程,在Consumer类中定义,简单地获取信息,然后将其打印出来,直到收到"DONE"字符串。这个线程也是随机停止。
import java.util.Random;
public class Consumer implements Runnable {
private Drop drop;
public Consumer(Drop drop) {
this.drop = drop;
}
public void run() {
Random random = new Random();
for (String message = drop.take();
! message.equals("DONE");
message = drop.take()) {
System.out.format("MESSAGE RECEIVED: %s%n", message);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
}
}
}
最后,主线程在ProducerConsumerExample中定义,它会启动生产者和消费者的线程。
注意:Drop类是为了说明守护代码块。为了避免重复发明轮子,在发明自己的数据共享类前,先研究下JAVA集合框架中已经有的数据结构。在问题和回答章节会有更多的涉及。