这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并且可能使用不再可用的技术。
请查看Java语言变更,了解Java SE 9及后续版本中更新的语言特性的摘要。
请查看JDK发行说明,了解所有JDK版本中的新功能、增强功能以及已删除或已弃用选项的信息。
线程经常需要协调它们的动作。最常见的协调方式是保护块。这样的块在开始时通过轮询条件来判断块是否可以继续执行。为了正确地执行这个过程,需要遵循一些步骤。
举个例子,假设guardedJoy
是一个方法,在另一个线程设置了共享变量joy
之前不能继续执行。理论上,这个方法可以简单地循环等待条件满足,但是这个循环是浪费的,因为它在等待期间会持续执行。
public void guardedJoy() { // 简单的循环保护。浪费处理器时间。不要这样做! while(!joy) {} System.out.println("已经获得了快乐!"); }
一个更高效的保护方式是使用Object.wait
来挂起当前线程。调用wait
不会返回,直到另一个线程发出通知,表示可能发生了某个特殊事件 - 尽管不一定是当前线程正在等待的事件:
public synchronized void guardedJoy() { // 这个保护只对每个特殊事件循环一次,可能不是我们正在等待的事件。 while(!joy) { try { wait(); } catch (InterruptedException e) {} } System.out.println("已经同时实现了快乐和效率!"); }
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 { // 从生产者发送到消费者的消息。 private String message; // 如果消费者应等待生产者发送消息,则为true;如果生产者应等待消费者检索消息,则为false。 private boolean empty = true; public synchronized String take() { // 等待直到消息可用。 while (empty) { try { wait(); } catch (InterruptedException e) {} } // 切换状态。 empty = true; // 通知生产者状态已改变。 notifyAll(); return message; } public synchronized void put(String message) { // 等待直到消息已检索。 while (!empty) { try { wait(); } catch (InterruptedException e) {} } // 切换状态。 empty = false; // 存储消息。 this.message = message; // 通知消费者状态已改变。 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[] = { "马吃燕麦", "母鹿吃燕麦", "小羊吃常春藤", "小孩也会吃常春藤" }; 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("接收到消息: %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(); } }