Java并发编程:使用Wait和Notify方法的注意事项

在之前的讲解线程状态的文章中,我们提到了waitnotify方法可以让线程在运行状态和等待状态之间转换。在这篇文章中,我们将深入探讨waitnotifynotifyAll方法在使用中的注意事项。我们主要从三个问题入手:

  1. 为什么wait方法必须在synchronized保护的代码中使用?

  2. 为什么wait方法需要在循环操作中使用?

  3. wait/notifysleep方法有什么异同?

1. 为什么wait()方法必须在synchronized修饰的代码中使用?

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

java 复制代码
public class QueueDemo {
    Queue<String> buffer = new LinkedList<String>();
    public void save(String data) {
        buffer.add(data);
        notify(); // 因为可能有线程在 take() 方法中等待
    }

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

在这段代码中,有两个方法。save方法负责向缓冲区添加数据,然后执行notify方法来唤醒之前等待的线程。take方法负责检查缓冲区是否为空。如果为空,线程进入等待状态;如果不为空,线程从缓冲区中取出数据。

这段代码没有使用synchronized保护,可能会出现以下情况:

  1. 首先,消费者线程调用take方法,并判断buffer.isEmpty是否返回true。如果返回true,表示缓冲区为空,线程准备进入等待状态。然而,在线程调用wait方法之前,它被可能已经被挂起了,wait方法没有执行。

  2. 此时,生产者线程开始运行,并执行了整个save方法。它向缓冲区添加了数据,并执行了notify方法,但notify没有效果,因为消费者线程的wait方法还没有执行,所以没有线程在等待被唤醒。

  3. 随后,之前被挂起的消费者线程恢复执行,并调用了wait方法,进入等待状态。

出现这个问题的原因是这里的"判断 - 执行"不是原子操作,它在中间被中断,是线程不安全的。

假设此时没有更多的生产者进行生产,消费者可能会陷入无限等待,因为它错过了save方法中的notify唤醒。

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

java 复制代码
public class QueueDemo2 {
    Queue<String> buffer = new LinkedList<>();
    public void save(String data) {
        System.out.println("Produce a data");
        buffer.add(data);
        notify(); // 因为可能有人在 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();
    }
}

你可以尝试执行这段代码,看看是否会出现之前提到的问题。

实际输出如下:

java 复制代码
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.lang.IllegalMonitorStateException异常。

修改代码:

java 复制代码
public class SyncQueueDemo2 {
    Queue<String> buffer = new LinkedList<>();
    public synchronized void save(String data) {
        System.out.println("Produce a data");
        buffer.add(data);
        notify(); // 因为可能有人在 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();
    }
}

再次执行代码,输出如下:

css 复制代码
Produce a data
Try to consume a data
Hello World!

可以看到,生产的"Hello World!"已经被成功消费并打印到控制台。

2. 为什么wait方法需要在循环操作中使用?

线程调用wait方法后,可能会出现虚假唤醒 (spurious wakeup)的情况,即线程在没有被notify/notifyAll调用、没有被中断、也没有超时的情况下被唤醒,这是我们不希望发生的情况。

虽然在真实环境中,虚假唤醒的概率非常小,但程序仍然需要在虚假唤醒的情况下保证正确性,因此需要使用while循环结构。

java 复制代码
while (条件不满足) {
    obj.wait();
}

这样,即使线程被虚假唤醒,如果条件不满足,wait会继续执行,从而消除虚假唤醒导致的风险。

3.wait/notifysleep方法有什么异同?

wait方法和sleep方法的相同点如下:

  1. 它们都可以阻塞线程。

  2. 它们都可以响应中断:如果在等待过程中收到中断信号,它们会响应并抛出InterruptedException异常。

它们之间也有很多不同点:

  1. wait方法必须在synchronized保护的代码中使用,而sleep方法没有这个要求。

  2. sleep方法在synchronized代码中执行时,它不会释放锁,而wait方法会主动释放锁。

  3. sleep方法需要定义一个时间,时间到期后线程会主动恢复。对于没有参数的wait方法,它意味着永久等待,直到被中断或唤醒,不会主动恢复。

  4. waitnotifyObject类的方法,而sleepThread类的方法。

好了,这次的内容就到这里,下次再见!

相关推荐
万物智能6 小时前
SARADC模数转换—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
前端·后端
花间相见6 小时前
【后端开发|网络编程】—— 五种实时通信方案全解析:短轮询、长轮询、SSE、MQTT、WebSocket
后端·网络协议
坤岭6 小时前
LLM应用安全护栏实战
后端
程序猿阿越6 小时前
containerd如何创建Pod
后端·kubernetes·源码阅读
名字还没想好☜6 小时前
Python sqlite3 实战:事务提交、参数化防注入、WAL 并发与 row_factory 取字典
后端·python·编程语言
MayBaymax6 小时前
Spring AI Alibaba Graph 快速上手:黑板、节点、边
java·spring·ai·ai编程
花间相见6 小时前
【AI应用开发|Agent记忆】—— Codex 与 Claude Code 持久化记忆对比:两条不用向量库的路线
后端·agent
葡萄城技术团队6 小时前
GcExcel V9.2 新特性揭秘:公式对象的无损往返
后端
斑鸠喳喳6 小时前
可重入锁 ReentrantLock
java·源码
IT枫斗者枫哥6 小时前
Spring AI 聊天记忆落库:重启后,怎样接上上一轮对话
java