Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
线程通常必须协调他们的操作。最常见的协调习惯用法是 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!"); }
更有效的保护方法是调用 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
不允许你指定被唤醒的线程,所以它只适用于大规模并行应用程序 也就是说,具有大量线程的程序,都在做类似的杂务。在这样的应用程序中,你不必关心哪个线程被唤醒。
让我们使用保护块来创建 Producer-Consumer 应用程序。这种应用程序在两个线程之间共享数据: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(); } }
生产者线程在
中定义,发送一系列熟悉的消息。字符串“DONE”表示已发送所有消息。为了模拟真实世界应用程序的不可预测性,生产者线程消息之间暂停随机的间隔。Producer
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"); } }
在
中定义的消费者线程只是简单地获取消息并将其打印出来,直到它获取到“DONE”字符串。该线程也会暂停随机的间隔。Consumer
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
类是为了演示防护块。为避免重新发明轮子,请在尝试编写自己的数据共享对象之前检查 Java 集合框架 中的现有数据结构。有关更多信息,请参阅 Questions and Exercises 部分。