Java 并发协作 wait、notify、notifyAll 方法

澳门新葡亰网站注册 1

wait, notify 和
notifyAll,这些在多线程中被经常用到的保留关键字,在实际开发的时候很多时候却并没有被大家重视。本文对这些关键字的使用进行了描述。

问:简单谈谈 Java 并发协作的 wait、notify、notifyAll 等方法的特点和场景?

答:首先并发协作的 wait、notify、notifyAll 等方法是定义在 java 的
Object 类中,而非 Thread。wait 有一个重载方法,参数 0
表示无限等待,更加重要的是在等待期间均可被中断并抛出
InterruptedException(很重要)。

每个对象都有一把锁和等待队列,线程在进入 synchronized
时,如果尝试获取锁但失败,就会把当前线程加入锁的等待队列,其实每个对象除过有用于锁的等待队列外还有一个条件队列,条件队列就是用来进行线程间的协作的。调用
wait 方法就会把当前线程放入这个条件队列并阻塞,然后等待其他线程通过
notify 或者 notifyAll 触发这个条件(自己无法触发)来执行,惟一的区别就是
notify 会从条件队列选择一个线程触发条件并且从队列移除,而 notifyAll
会触发条件队列里所有等待的线程并从队列移除。

wait 和 notify、notifyAll 只能在 synchronized
函数或者对象中调用,被上锁的对象一般是多线程共享的对象,如果调用 wait
和 notify、notifyAll 方法时当前线程没有持有对象锁则会抛出
IllegalMonitorStateException 异常。

切记代码执行到 synchronized 锁起来的 wait
方法时当前线程会释放对象锁,
因为 wait
的具体实现过程是先把当前线程放入条件等待队列、释放对象锁、阻塞等待(线程状态变为
WAITING 或 TIMED_WATING),等待时间到了或者被其他线程 notify、notifyAll
以后从条件队列中移除。然后要重新竞争对象锁,竞争到就变为 RUNNABLE
状态,否则该线程被加入对象锁队列变为 BLOCKED 状态。

切记调用 notify、notifyAll
会把条件队列中等待的线程移除但是不会释放对象锁,只有在包含
notify、notifyAll 的 synchronized
方法或者代码块执行完毕才能轮到等待的线程执行。

除了我们要保证 wait 和 notify、notifyAll 应该在 synchronized
块中和那个被多线程共享的对象上调用以外,还要尽可能保证永远在条件循环而不是
if 语句中使用 wait,因为线程从 wait
调用中返回后不代表其等待的条件就一定成立,
所以我们在使用 wait
时应该尽量使用如下模板:

        synchronized (sharedObject) {
            while (condition) {
                sharedObject.wait();
                // (Releases lock, and reacquires on wakeup)
            }
            // do action based upon condition e.g. take or put into queue
        }
    }

在条件循环里使用 wait
的目的是在线程被唤醒的前后,都持续检查条件是否被满足,如果条件并未改变而
wait 被调用之前 notify
的唤醒通知就来了,那么这个线程并不能保证被唤醒且有可能会导致死锁问题(建立在全局项目超过两个线程以上)。

譬如假设有两个生产者 A、B,一个消费者 C,在生产消费者模式中如果对生产者
A、B 不使用条件循环而简单 if 判断中调用 wait 就会出事,当空间满了后 A、B
都被 wait,当 C 取走一个数据后如果调用了 notifyAll 则 A、B
都将被唤醒,假设 A 被唤醒后往空间放入一个数据且空间满了,而此时 B
也会放置一个数据,所以发生空间炸裂错误。

(提示:如上也解答了并发的另一个面试题,即 Java 多线程为什么使用 while
循环来调用 wait 方法?)其实 Thread 的 join
方法实现也是条件循环,核心代码是:

while (isAlive()) {
  lock.wait(0);
  }

并发协作其实在 java.util.concurrent
包下已经提供了很多不错且高效的封装实现类了,不过我们依然可以自己使用
wait 和 notify、notifyAll 来解决生产消费者场景、并发等待等场景问题。

在 Java 中可以用 wait、notify 和 notifyAll
来实现线程间的通信。。举个例子,如果你的Java程序中有两个线程——即生产者和消费者,那么生产者可以通知消费者,让消费者开始消耗数据,因为队列缓冲区中有内容待消费(不为空)。相应的,消费者可以通知生产者可以开始生成更多的数据,因为当它消耗掉某些数据后缓冲区不再为满。

我们可以利用wait()来让一个线程在某些条件下暂停运行。例如,在生产者消费者模型中,生产者线程在缓冲区为满的时候,消费者在缓冲区为空的时候,都应该暂停运行。如果某些线程在等待某些条件触发,那当那些条件为真时,你可以用
notify 和 notifyAll
来通知那些等待中的线程重新开始运行。不同之处在于,notify
仅仅通知一个线程,并且我们不知道哪个线程会收到通知,然而 notifyAll
会通知所有等待中的线程。换言之,如果只有一个线程在等待一个信号灯,notify和notifyAll都会通知到这个线程。但如果多个线程在等待这个信号灯,那么notify只会通知到其中一个,而其它线程并不会收到任何通知,而notifyAll会唤醒所有等待中的线程。

在这篇文章中你将会学到如何使用 wait、notify 和 notifyAll
来实现线程间的通信,从而解决生产者消费者问题。如果你想要更深入地学习Java中的多线程同步问题,我强烈推荐阅读Brian
Goetz所著的《Java Concurrency in
Practice澳门新葡亰网站注册, | Java
并发实践》,不读这本书你的
Java 多线程征程就不完整哦!这是我最向Java开发者推荐的书之一。

如何使用Wait

尽管关于wait和notify的概念很基础,它们也都是Object类的函数,但用它们来写代码却并不简单。如果你在面试中让应聘者来手写代码,用wait和notify解决生产者消费者问题,我几乎可以肯定他们中的大多数都会无所适从或者犯下一些错误,例如在错误的地方使用
synchronized
关键词,没有对正确的对象使用wait,或者没有遵循规范的代码方法。说实话,这个问题对于不常使用它们的程序员来说确实令人感觉比较头疼。

第一个问题就是,我们怎么在代码里使用wait()呢?因为wait()并不是Thread类下的函数,我们并不能使用Thread.call()。事实上很多Java程序员都喜欢这么写,因为它们习惯了使用Thread.sleep(),所以他们会试图使用wait()
来达成相同的目的,但很快他们就会发现这并不能顺利解决问题。正确的方法是对在多线程间共享的那个Object来使用wait。在生产者消费者问题中,这个共享的Object就是那个缓冲区队列。

第二个问题是,既然我们应该在synchronized的函数或是对象里调用wait,那哪个对象应该被synchronized呢?答案是,那个你希望上锁的对象就应该被synchronized,即那个在多个线程间被共享的对象。在生产者消费者问题中,应该被synchronized的就是那个缓冲区队列。(我觉得这里是英文原文有问题……本来那个句末就不应该是问号不然不太通……)

澳门新葡亰网站注册 1