保护块(Guarded Blocks)
线程间经常要协调并行操作,而最常见的协调方法就是保护块。保护块即:一块在执行前必须检查是否满足某一条件的代码。要做到这一点,要走这几步。
举个例子guardedJoy 是一个共享变量joy被置为true时才能执行的方法。理论上讲,guardedJoy可以写成一直循环判断joy知道joy为true。但循环很浪费资源,因为在等待执行期间它一直占用着CPU时间。
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调用会一直挂起当前线程直到另一线程通知挂起线程有些特殊事件发生了——即使这个事件并不是挂起线程期待的:(译者注:所以你不能用if只判定一次条件而是用while确认当前条件确实满足了执行的条件,因为很可能唤醒你的线程并不清楚你在等待什么)
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!");
}
注意:永远把守护卫士做成一个while循环而不是if判断。不要假设唤醒你的线程将你的执行条件置为true,或者执行条件会在唤醒后一直是true。
像很多挂起线程的方法一样,wait会抛出InterruptedException。在本例中,我们只是简单的忽略该异常——我们只关心joy的值。
为什么guardeJoy方法是同步的?假设d是wait所属的对象,当一个线程调用d.wait时,它必须拥有了d的内含锁(intrinsic lock)——否则将会抛出运行时异常。同步方法是获取内含锁的简单方法。
当wait被调用后,线程会释放锁并挂起。在后来某一时间,另一个线程会获取相同的锁,并调用notifyAll,通知所有等待该锁的线程有重要事情发生了:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
再后来,第二个线程释放了该锁,第一个线程再次获取该锁从wait中恢复。
注意:有第二个通知方法,notify,只能唤醒一个线程。因为notify不能指定哪个线程被唤醒,所以它只在大规模并发程序(massively parallel applications)中使用——有大量线程做同样的操作。在这种程序中,你不关心哪个线程被唤醒。
我们使用保护块来创建一个生产者消费者程序(Producer-Consumer)程序。这种程序在两个线程间共享数据:生产者生产数据,消费者消费数据。两个线程使用共享对象通信。协调是很必要的:生产者线程在生产者生产数据之前不能试图得到数据。生产者不能在旧数据未被消费之前交付新数据。
在本例中,共享数据是一系列的文本消息,通过一个叫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,加载了生产者和消费者线程。
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类是为了描述保护块而写的。为了避免重复造轮子(re-inventing the wheel),在你写自己的数据共享对象之前尝试一下使用Java Collections Framework中已经定义好的数据结构。欲知详情,请看Quesitons and Exercises 一节。