03_concurrency_05_wait_notify

第三十九章 Wait和Notify机制

1 概述

Wait和Notify是Java最基础的线程同步机制。在该同步机制中,通过使用Wait和Notify两类操作搭建起保护代码区间(Guarded Block),以使得程序正确运行,避免竞态条件发生。

Wait/Notify机制由Object类中的五个成员方法组成。调用Wait方法后,线程将进入等待状态,直到线程被唤醒。而调用Notify方法会唤醒等待该对象的线程。

Wait方法有下面三种形式。调用第一个wait()方法会将当前线程进入等待状态,直到被唤醒。如果没有被通知的话,该线程将永远等待下去。而第二个和第三个wait()方法允许调用者设置一个最长等待时间,如果在该时间段内未被唤醒的话,wait()方法立即返回,当前线程可以继续执行下一条语句。

public class Object {
    ...
    public void wait();
    public void wait(long timeout);
    public void wait(long timeout, int nanos);
    ...
}

Notify方法有两种形式。notify()方法会唤醒某一个等待该对象的线程。哪一个线程被唤醒由Java的具体实现决定。Java语言给与了充分的自由。notifyAll()方法则唤醒所有等待该对象的线程。

public class Object {
    ...
    public void notify();
    public void notifyAll();
    ...
}

2 Wait/Notify的使用方法

Wait/Notify使用方法非常复杂,有许多实现细节需要考虑。有时,Wait/Notify机制被人们称为Java多线程的“汇编语言”。因此,小水滴并不建议读者使用Wait/Notify机制实现线程同步。绝大多数Java多线程程序是由其他同步机制或者使用标准库实现的。但是,本系列文章还是收录了Wait/Notify,这是因为Wait/Notify是一种最为基础的同步方式。理解Wait/Notify机制有助于帮助理解Java多线程编程的复杂性和增强分析问题的能力。

在使用上,Wait/Notify机制有如下要求和细节。

  1. 首先,在调用Wait/Notify方法之前,线程需要事先获得该对象的monitor。获得对象monitor的方式是使用synchronized成员方法、synchronized静态方法、或者synchronized语句。其细节可参考synchronized的使用方法
  2. 在调用wait()方法后,当前线程会进入等待状态。Java虚拟机会首先释放对象的monitor锁,然后将其加入该对象的等待线程集合中。释放monitor锁的原因是因为当前线程进入了等待状态,其他线程需要获取monitor锁,以获取调用notify()方法的机会。
  3. 当线程被通知(notify)唤醒后,Java虚拟机首先会尝试再次获取对象的monitor锁。如果获取成功,则该线程继续执行。如果不成功,则该线程继续等待,直到获取成功。
  4. 在使用wait()方法时,开发人员还需要考虑“假唤醒(spurious wakeup)”。假唤醒的意思是:线程并没有被通知(Notify)唤醒,但是该线程被唤醒了。更多的开发人员认为假唤醒是一种硬件/软件错误。但是,由于各种历史原因和平台兼容性等原因,在Java环境下,假唤醒是有可能发生的。所以,程序一般会将wait()方法放置在一个循环中调用。当线程被唤醒时,需要再次确认一下唤醒的条件是否满足。
  5. 在调用notify()/notifyAll()方法之前,线程需要事先获取该对象的monitor锁。

3 生产者/消费者实例

最后,我们用一个生产者/消费者实例来展示wait/notify机制的使用方法。使用其他机制实现生产者/消费者的例子可参见第二十六章原子类和并发容器。ProducerConsumerPatternExample类包含了一个共享的整形队列sharedQueue,MAX_LEN指定了该队列能容纳的最多元素个数。方法produce()不停的向队列添加整数10,直到队列被填满。方法consume()不停的从队列中取出整数,直到队列被取空。

值得注意的是,produce()和consume()方法都将wait()和notifyAll()方法的调用放在synchronized语句块中,这是因为在调用wait()和notifyAll()方法时,该线程需要事先获得sharedQueue的monitor的拥有权(ownership)。其次,它们将wait()方法调用放在while循环中是为了防备“假唤醒”发生。如果“假唤醒”发生了,线程需要再次检查唤醒条件是否满足。因为produce()和consume()的绝大部分代码都在synchronized代码块中,在同一时刻,只有一个线程才能运行synchronized代码块中的代码。

import java.util.Queue;
import java.util.LinkedList;

public class ProducerConsumerPatternExample {
    private Queue<Integer> sharedQueue = new LinkedList<Integer>();
    private final int MAX_LEN = 3;

    public void produce() {
        while(true) {
            // 获取sharedQueue的monitor
            synchronized (sharedQueue) {
                // 防止“假唤醒”发生,在wait()方法返回时,还需要再检查一些唤醒条件是否满足
                while (sharedQueue.size() == MAX_LEN) {
                    try {
                        System.out.println("Queue is full. Producer Waits.");
                        // 队列已满,等待Consumer移除元素,并唤醒自己
                        sharedQueue.wait();
                    } catch (Exception ex) {}
                }

                // 填充元素
                sharedQueue.add(10);
                System.out.println("Producing integer 10");
                
                // 通知 Consumer
                sharedQueue.notifyAll();
            }
        }
    }

    public void consume() {
        for (;;) {
            // 获取sharedQueue的monitor
            synchronized (sharedQueue) {
                // 防止“假唤醒”发生,在wait()方法返回时,还需要再检查一些唤醒条件是否满足
                while (sharedQueue.isEmpty()) {
                    try {
                        System.out.println("Queue is empty. Consumer Waits.");
                        // 队列已空,等待Producer填充元素,并唤醒自己
                        sharedQueue.wait();
                    } catch (Exception ex) {}
                }

                // 移除元素
                Integer i = sharedQueue.remove();
                System.out.println("Consuming " + i);
                
                // 通知 Producer
                sharedQueue.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        // 在同一个ProducerConsumerPatternExample对象上运行两个线程
        // 这两个线程分别运行produce()方法和consume()方法
        ProducerConsumerPatternExample example = new ProducerConsumerPatternExample();
        Runnable producer = () -> {example.produce();};
        Runnable consumer = () -> {example.consume();};

        Thread producerThread = new Thread(producer);
        Thread consumerThread = new Thread(consumer);
        producerThread.start();
        consumerThread.start();
        producerThread.join();
        consumerThread.join();
    }
}

程序运行结果如下:

> java ProducerConsumerPatternExample
Producing integer 10
Producing integer 10
Producing integer 10
Queue is full. Producer Waits.
Consuming 10
Consuming 10
Consuming 10
Queue is empty. Consumer Waits.
Producing integer 10
Producing integer 10
Producing integer 10
Queue is full. Producer Waits.
Consuming 10
Consuming 10
Consuming 10
...

4 总结

本小节讲解了Wait/Notify机制。虽然小水滴并不推荐直接使用Wait/Notify机制,但是学习这个机制有助于加深理解多线程编程的复杂性,以帮助学习后续介绍的其他线程同步机制。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.