Java并发编程实战 04 | 使用Wait&Notify时要注意什么?

在 Java 中,wait()、notify() 和 notifyAll() 方法在多线程编程中主要用于线程间的协作和同步。理解这些方法的使用特点对于编写稳定的多线程程序至关重要。我们将从以下三个问题入手深入探讨它们的使用:

  1. 为什么必须在 synchronized 代码块中使用 wait() 方法?
  2. 为什么 wait 方法需要在循环中使用?
  3. wait/notify 和 sleep 方法之间的相似点和不同点?

为什么必须在 synchronized 代码块中使用 wait() 方法?

为了找到这个问题的答案,让我们反过来思考:如果我们不要求在synchronized代码块中使用wait方法,会发生什么问题?让我们看看下面这段代码。

public class QueueDemo {

    Queue<String> buffer = new LinkedList<String>();

    public void save(String data) {
        buffer.add(data);
        
        //因为可能有线程在等待,所以需要通知它们
        notify();  
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }
}

这段代码中主要有两个方法:save() 和 take()。save() 方法负责将数据添加到 buffer 中,并调用 notify() 方法来唤醒因为buffer为空而等待的线程。

take() 方法则检查 buffer 是否为空。如果为空,则调用wait()方法进入等待状态;如果不为空,则从 buffer 中获取一个数据项。

这是一个典型的生产者-消费者模式,关于生产者-消费者问题将在后续的文章中详细探讨。

然而,这段代码没有受到 synchronized 关键字的保护,可能会出现以下情况:

  • 首先,消费者线程调用take方法,在take方法中通过buffer.isEmpty()判断buffer是否为空,如果为空,线程要调用wait方法进入等待状态,但是如果线程在调用wait方法之前就被调度器挂起了,此时方法wait还未执行。

  • 与此同时,生产者线程开始运行,并执行 save 方法。它向 buffer 中添加数据,并调用 notify 方法。然而,由于消费者线程的 wait 方法还未执行,因此notify 调用没有任何效果,因为没有任何线程在等待唤醒。

  • 接着,之前被调度器挂起的消费者线程恢复执行,并调用 wait 方法,进入等待状态。错过了先前的唤醒,在这种情况下,消费者线程会继续等待,直到生产者线程再次调用 notify 或 notifyAll,才能唤醒消费者线程。

你可以使用两个线程分别调用这两个方法,一个模拟生产者线程,一个模拟消费者线程:

public class QueueDemo2 {

    Queue<String> buffer = new LinkedList<String>();

    public void save(String data) {
        System.out.println("Produce a data");
        buffer.add(data);
        notify();  // Since someone may be waiting in take()
    }

    public String take() throws InterruptedException {
        System.out.println("Try to consume a data");
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        QueueDemo2 queueDemo = new QueueDemo2();

        Thread producerThread = new Thread(() -> {
            queueDemo.save("Hello World!");
        });

        Thread consumerThread = new Thread(() -> {
            try {
                System.out.println(queueDemo.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        consumerThread.start();
        producerThread.start();
    }
}

//输出:
Try to consume a data
Produce a data
Exception in thread "Thread-0" Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
    at java.lang.Object.notify(Native Method)
    at thread.basic.chapter4.QueueDemo2.save(QueueDemo2.java:13)
    at thread.basic.chapter4.QueueDemo2.lambda$main$0(QueueDemo2.java:28)
    at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at thread.basic.chapter4.QueueDemo2.take(QueueDemo2.java:19)
    at thread.basic.chapter4.QueueDemo2.lambda$main$1(QueueDemo2.java:33)

值得庆幸的是,你根本没有犯错的机会!因为如果 wait 方法和 notify 方法在没有被 synchronized 关键字保护的代码块中执行,Java 会直接抛出 java.lang.IllegalMonitorStateException 异常。

为了解决这个问题,我们需要对代码进行修改:

public class SyncQueueDemo2 {

    Queue<String> buffer = new LinkedList<>();

    public synchronized void save(String data) {
        System.out.println("Produce a data");
        buffer.add(data);
        notify();  // Since someone may be waiting in take()
    }

    public synchronized String take() throws InterruptedException {
        System.out.println("Try to consume a data");
        while (buffer.isEmpty()) {
            wait();
        }
        return buffer.remove();
    }

    public static void main(String[] args) throws InterruptedException {
        SyncQueueDemo2 queueDemo = new SyncQueueDemo2();

        Thread producerThread = new Thread(() -> {
            queueDemo.save("Hello World!");
        });

        Thread consumerThread = new Thread(() -> {
            try {
                System.out.println(queueDemo.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        consumerThread.start();
        producerThread.start();
    }
}

//输出:
Produce a data
Try to consume a data
Hello World!

可以看到,程序成功运行,并将“Hello World!”正确打印到控制台。

为什么 wait 方法需要在循环中使用?

为了回答这个问题,我们先来了解一个概念:虚假唤醒。

“虚假唤醒”(spurious wakeup)是多线程编程中一个重要的概念,它指的是一个线程在调用 wait 方法后,没有接收到 notify 或 notifyAll 调用的情况下被意外唤醒。虚假唤醒的原因可能是底层操作系统或线程库的实现细节。这是我们不希望看到的。

尽管在实际环境中发生虚假唤醒的概率很小,但程序仍然需要确保在这种情况下的正确性。因此,我们使用 while 循环结构来反复检查等待条件,从而保证线程在被唤醒时,只有在条件满足的情况下才会继续执行。

while (condition does not hold)
    obj.wait();

这样,即便是被误唤醒了,也会再次检查 while 中的条件,如果条件不满足,则继续 调用wait()等待,这种做法能够处理虚假唤醒的情况。

wait/notify 和 sleep 方法之间的相似点和不同点?

以下是 wait 方法和 sleep 方法之间的相似之处:

  1. 阻塞线程:wait 和 sleep 都会导致当前线程进入阻塞状态。
  2. 响应中断:如果在等待过程中收到中断信号,两者都会响应并抛出 InterruptedException 异常。

但是,它们之间也存在着许多不同之处:

  1. 使用位置不同:wait 方法必须在 synchronized 修饰的代码块或方法中使用,而 sleep 方法没有这个要求,可以在任何地方使用。
  2. 锁处理方式不同:当 wait 方法执行时,线程会主动释放所持有的对象锁;而 sleep 方法不会释放锁,即使它是在同步代码块中执行。
  3. 恢复机制不同:sleep 方法需要指定一个时间,时间到后线程会自动恢复;而 wait 方法(不带参数的情况)表示线程将永久等待,直到被中断或被其他线程唤醒。
  4. 所属类不同:wait 和 notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。
  • 21
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值