线程必须经常协调他们的活动。最普通的协调方法是保护块(guarded block)。以循环条件开始的代码块必须在循环条件为真时代码块才能够执行。为了正确操作有许多步骤需要遵守。
假设,比如 guardedJoy方法直到共享变量joy被另一个线程修改才会执行。理论上,这个方法一直在循环直到满足条件,但是这个循环很浪费,因为它在等待的时候不停的在运行。
public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy) {}
System.out.println("Joy has been achieved!");
}
一个更有效的保护(guard)是调用Object.wait去暂停当前线程。wait的调用不会返回直到其他的线程发出某个特别的事件已经发生的通知-虽然不见得是这个线程正在等待的事件。(有可能是发生了异常)
public synchronized 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方法。不要假设中断(interrupt)是你要等待的特定条件,因为条件仍然还是true
像许多暂停执行的方法,wait能够抛出InterruptedException。在这个例子中,我们能够忽略这个异常-我们仅仅只关心joy的值
为什么这个版本的guardJoy要同步?假设d是我们用来调用wait的对象。当一个线程调用d.wait,它肯定拥有d的内置锁-否则会抛出一个错误。在同步方法里调用wait方法是一个获得内置锁的简单方法
当wait被调用,线程会释放掉锁并且暂停执行。在未来的某个时间,另外一个线程将会获得同样的锁并且调用Object.notifyAll,这会通知所有在等待lock的线程有一些重要的事情发生了
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
有时在第二个线程释放掉锁后,第一个线程重新获得这个锁并且从调用的wait中返回恢复运行。
注意:有第二个通知方法,notify,只能唤醒一个线程。因为notify不允许你指定要唤醒的线程,这个在大规模并行程序中有用处-即,程序中有大量的线程,并且都在做类似的事情,在这样的程序中,你不必在意哪个线程被唤醒了。
让我们使用保护块(guarded blocks)来创建一个生产者-消费者应用程序。这类程序在线程间共享数据:生产者,负责生产数据,消费者,负责使用数据。这2个线程使用共享对象通信。协调是必不可少的:消费者不能够在生产者递送数据前检索它,并且如果消费者还没有取得老数据生产者不能够递送新数据
在这个例子中,数据是一系列的文本消息,通过一个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) {}
}
}
}
最后,这是main线程,定义在ProducerConsumerExample中,并且启动生产者和消费者线程
public class ProducerConsumerExample {
public static void main(String[] args) {
Drop drop = new Drop();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
注意:Drop是用来演示保护快的。为了避免重复,在写你自己数据分享对象之前,看一下java集合框架中已有的数据结构。更多信息,看问题和练习一章。