文档

Java™ 教程
隐藏目录
保护块
路径:基本的Java类
课程:并发

保护块

线程经常需要协调它们的动作。最常见的协调方式是保护块。这样的块在开始时通过轮询条件来判断块是否可以继续执行。为了正确地执行这个过程,需要遵循一些步骤。

举个例子,假设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();
    }
}

生产者线程在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[] = {
            "马吃燕麦",
            "母鹿吃燕麦",
            "小羊吃常春藤",
            "小孩也会吃常春藤"
        };
        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("接收到消息: %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集合框架中的现有数据结构。有关更多信息,请参阅问题和练习部分。

上一页: 饥饿和活锁
下一页: 不可变对象