线程间通信
为什么会有等待通知机制
无限循环,浪费CPU资源,而解决死锁的思路之一就是 破坏请求和保持条件
——多个线程实现互斥访问共享资源时会互相发送信号或等待信号,比如线程等待数据到来的通知,线程收到变量改变的信号等。
医院就医过程:
序号 | 就医 | 程序解释(自己的视角) |
---|---|---|
1 | 挂号成功,到诊室门口排号候诊 | 排号的患者(线程)尝试获取【互斥锁】 |
2 | 大夫叫到自己,进入诊室就诊 | 自己【获取到互斥锁】 |
3 | 大夫简单询问,要求做检查(患者缺乏报告不能诊断病因) | 进行【条件判断】,线程要求的条件【没满足】 |
4 | 自己出去做检查 | 线程【主动释放】持有的互斥锁 |
5 | 大夫叫下一位患者 | 另一位患者(线程)获取到互斥锁 |
6 | 自己拿到检测报告 | 线程【曾经】要求的条件得到满足(实则【被通知】) |
7 | 再次在诊室门口排号候诊 | 再次尝试获取互斥锁 |
8 | … | … |
Java语言中,其内置的关键字 synchronized
和 方法wait()
、notify()/notifyAll()
就能实现上面提到的等待/通知机制:
注意:
-
一个锁对应一个【入口等待队列】,不同锁的入口等待队列没任何关系,不存在竞争关系。(不同患者进入眼科和耳鼻喉科看大夫一点冲突都没有)
-
wait()
、notify()/notifyAll()
要在synchronized内部
被使用,并且,如果锁的对象是this,就要this.wait(),this.notify()/this.notifyAll()
, 否则JVM就会抛出java.lang.IllegalMonitorStateException
。因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说
notify/notifyAll
和wait
方法依赖于monitor对象,而monitor存在于对象头的Mark Word中(存储monitor引用指针),而synchronized
关键字可以获取monitor
。
生产者-消费者模型
一个内存队列,多个生产者线程
往内存队列中放数据
;多个消费者线程
从内存队列中取数据
。要实现这样一个编程模型,需要做下面几件事情:
- 内存队列本身要加锁,才能实现线程安全;
- 阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事可做,会被阻塞。
- 线程自己阻塞自己,也就是生产者、消费者线程各自调用
wait()
和notify()
; - 用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的。这也就是
BlockingQueue
的实现。
- 线程自己阻塞自己,也就是生产者、消费者线程各自调用
- 双向通知。消费者被阻塞之后,生产者放入新数据,要通知消费者;反之,生产者被阻塞之后,消费者消费了数据,要通知生产者。
wait()
和notify()
机制Condition
机制
wait和notify
wait
和notify
方法并不是Thread特有的方法,而是Object中的方法!
public class Object {
public final void wait() throws InterruptedException {
wait(0); // 0代表永不超时
}
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
public final native void wait(long timeout) throws InterruptedException;
public final native void notify();
public final native void notifyAll();
}
wait
-
wait方法必须拥有该对象的monitor,也就是wait方法必须在同步方法中使用。
-
当前线程执行了该对象的wait方法之后,将会放弃对该monitor的所有权并且进入与该对象关联的wait set中,也就是说一旦线程执行了某个object的wait方法之后,它就会释放对该对象monitor的所有权,其他线程也会有机会继续争抢该monitor的所有权。Object的
wait(long timeout)方法
会导致当前线程进入阻塞,直到有其他线程调用了Object的notify
或者notifyAll
方法才能将其唤醒,或者阻塞时间到达了timeout时间而自动唤醒。——释放锁;阻塞,等待被其他线程唤醒;重新拿锁! -
wait方法是可中断方法,这也就意味着,当前线程一旦调用了wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断的;可中断方法被打断后会收到中断异常
InterruptedException
,同时interrupt标识也会被擦除。
notify
随机唤醒一个:一个线程调用共享对象的notify()
方法,会唤醒一个在该共享变量上调用wait()
方法后被挂起的线程,一个共享变量上可能有多个线程在等待,具体唤醒那一个,是随机的。
- 唤醒单个正在执行该对象wait方法的线程。
- 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略。
- 被唤醒的线程,需要重新获取对该对象所关联monitor的lock才能继续执行。
- notify方法必须拥有该对象的monitor,也就是notify方法必须在同步方法中使用。
使用notify
方法的情景——线程池:
- 所有等待线程拥有相同的等待条件;
- 所有等待线程被唤醒后,执行相同的操作;
- 只需唤醒一个线程。
wait和sleep
相同点
- wait和sleep方法都可以使线程进入阻塞状态;
- wait和sleep方法均是可中断方法,由于两个方法进入阻塞状态的线程,被中断后都会收到中断异常。
不同点
- wait是
Object
的方法,而sleep是Thread
特有的方法; - wait方法的执行必须在同步方法中进行,而sleep则不需要;
- 线程在同步方法中执行sleep方法时,并不会释放monitor的锁,而wait方法则会释放monitor的锁;
- sleep方法短暂休眠之后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞。
单线程间通信
服务端有若干个线程,会从队列中获取相应的Event进行异步处理,那么这些线程又是如何从队列中获取数据的呢?
- 不断地轮询:如果有数据,则读取数据并处理;如果没有则等待若干时间,再次轮询;
- 通知机制:如果队列中有Event,则通知工作的线程开始工作;没有Event,则工作线程休息并等待通知。
实现一个EventQueue
:
- 队列满——最多可容纳多少个Event;
- 队列空——当所有的Event都被处理,并且没有新的Event被提交的时候;
- 有Event但是没有满。
package communication;
import java.util.LinkedList;
import static java.lang.Thread.currentThread;
/**
* 多个线程向队列添加元素
*/
public class EventQueue
{
private final int max;
static class Event {
}
// 共享的资源
private final LinkedList<Event> eventQueue = new LinkedList<>();
private final static int DEFAULT_MAX_EVENT = 5;
public EventQueue() {
this(DEFAULT_MAX_EVENT);
}
public EventQueue(int max) {
this.max = max;
}
/**
* 向队列添加事件
* @param event 事件
*/
public void offer (Event event) {
synchronized (eventQueue) {
// 注意这里使用的if
if (eventQueue.size() >= max) {
try {
console(" the queue is full.");
// 队列已满,执行提交的线程开始等待
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console(" the new event is submitted");
eventQueue.addLast(event);
// 通知所有被阻塞的线程,可以开始执行相关操作
eventQueue.notify();
}
}
/**
* 从队列中取出元素
* @return 取出的事件
*/
public Event take() {
synchronized (eventQueue) {
// 注意这里使用的if
if (eventQueue.isEmpty()) {
try {
console(" the queue is empty.");
// 队列是空的,执行提取的线程开始等待
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeFirst();
// 通知所有被阻塞的线程,可以开始执行相关操作
this.eventQueue.notify();
console(" the event " + event + " is handled.");
return event;
}
}
private void console(String message) {
System.out.printf("%s:%s\n", currentThread().getName(), message);
}
}
在EventQueue
中定义了一个队列
offer方法
会提交一个Event
至队尾,如果此时队列已经满了,那么提交的线程将会被阻塞,这是调用了wait方法
的结果。take方法
会从队头获取数据,如果队列中没有可用数据,那么工作线程就会被阻塞,这也是调用wait方法
的直接结果。notify方法
的作用是唤醒那些曾经执行monitor的wait方法而进入阻塞的线程。
package communication;
import java.util.concurrent.TimeUnit;
public class EventClient
{
public static void main(String[] args)
{
final EventQueue eventQueue = new EventQueue();
// 单线程提交
for (int i = 1; i < 2; i++) {
new Thread(() ->
{
for (; ; )
{
eventQueue.offer(new EventQueue.Event());
// 防止新的线程还没来得及创建,而一直执行旧线程
try
{
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}, "Producer-" + i).start();
}
// 单线程获取并处理
for (int i = 1; i < 2; i++) {
new Thread(() ->
{
for (; ; )
{
eventQueue.take();
// 防止新的线程还没来得及创建,而一直执行旧线程
try
{
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}, "Consumer-" + i).start();
}
}
}
输出结果:
Consumer-1: the queue is empty.
Producer-1: the new event is submitted
Consumer-1: the event communication.EventQueue$Event@cc8b1a is handled.
Producer-1: the new event is submitted
Consumer-1: the event communication.EventQueue$Event@1aad818 is handled.
Consumer-1: the queue is empty.
Producer-1: the new event is submitted
Consumer-1: the event communication.EventQueue$Event@332131 is handled.
Consumer-1: the queue is empty.
Producer-1: the new event is submitted
...
多线程间通信
package communication;
import java.util.concurrent.TimeUnit;
public class EventClient
{
public static void main(String[] args)
{
final EventQueue eventQueue = new EventQueue();
// 多线程提交
for (int i = 1; i < 3; i++) {
new Thread(() ->
{
for (; ; )
{
eventQueue.offer(new EventQueue.Event());
// 防止新的线程还没来得及创建,而一直执行旧线程
try
{
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}, "Producer-" + i).start();
}
// 多线程获取并处理
for (int i = 1; i < 3; i++) {
new Thread(() ->
{
for (; ; )
{
eventQueue.take();
// 防止新的线程还没来得及创建,而一直执行旧线程
try
{
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}, "Consumer-" + i).start();
}
}
}
如果对EventQueue.java
不做修改,会出现:
- Linkedlist元素为5时执行addlast方法(将
take线程
sleep时间设置得比offer线程
sleep时间长)
Consumer-1: the queue is empty.
Producer-2: the new event is submitted
Producer-1: the new event is submitted
Consumer-2: the event communication.EventQueue$Event@12b9c83 is handled.
Consumer-1: the event communication.EventQueue$Event@1330f48 is handled.
Producer-1: the new event is submitted
Producer-2: the new event is submitted
Producer-1: the new event is submitted
Producer-2: the new event is submitted
Producer-1: the new event is submitted
Producer-2: the queue is full.
Producer-1: the queue is full. // 此时已经满了:5
Consumer-2: the event communication.EventQueue$Event@1ec6496 is handled. // 此时为4
Producer-2: the new event is submitted // 此时已经满了:5
Producer-1: the new event is submitted // 又添加了,出现问题
假设某个时刻EventQueue
中存在5个Event数据,其中两个线程在执行offer方法的时候分别因为调用了wait方法而进入阻塞中,另外的一个线程执行take方法消费了event元素并且唤醒了一个offer线程,而该offer线程执行了addLast方法之后,queue中的元素为5,并且再次执行唤醒方法,恰巧另外一个offer线程也被唤醒,因此可以绕开阀值检查eventQueue.size() >= max
(由于是if
语句来判断,阻塞的线程被唤醒后,是从eventQueue.wait();
往后开始继续执行的),致使EventQueue中的元素超过5个。
- Linkedlist为空时执行removeFirst方法(将
take线程
sleep时间设置得比offer线程
sleep时间短)
Consumer-1: the queue is empty.
Consumer-2: the queue is empty.
Exception in thread "Consumer-2" java.util.NoSuchElementException
at java.util.LinkedList.removeFirst(LinkedList.java:270)
at communication.EventQueue.take(EventQueue.java:72)
...
假设EventQueue
中的元素为空,两个线程在执行take方法时分别调用wait方法进入了阻塞之中,另外一个offer线程执行addLast方法之后唤醒了其中一个阻塞的take线程,该线程顺利消费了一个元素之后恰巧再次唤醒了一个take线程,这时就会导致执行空LinkedList的removeFirst方法。
生产者消费者实例(多线程间通信)
wait标准范式:
synchronized() {
while(条件不满足) {
wait();
}
}
将临界值判断语句if
改成while
,将notify
改成notifyAll
:
package communication;
import java.util.LinkedList;
import static java.lang.Thread.currentThread;
/**
* 多个线程向队列添加元素
* @author Chenzf
*/
public class EventQueue
{
private final int max;
static class Event {
}
// 共享的资源
private final LinkedList<Event> eventQueue = new LinkedList<>();
private final static int DEFAULT_MAX_EVENT = 5;
public EventQueue() {
this(DEFAULT_MAX_EVENT);
}
public EventQueue(int max) {
this.max = max;
}
/**
* 向队列添加事件
* @param event 事件
*/
public void offer (Event event) {
synchronized (eventQueue) {
// 注意这里使用的while
while (eventQueue.size() >= max) {
try {
console(" the queue is full.");
// 队列已满,执行提交的线程开始等待
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console(" the new event is submitted");
eventQueue.addLast(event);
// 通知所有被阻塞的线程,可以开始执行相关操作
eventQueue.notifyAll();
}
}
/**
* 从队列中取出元素
* @return 取出的事件
*/
public Event take() {
synchronized (eventQueue) {
// 注意这里使用的while
while (eventQueue.isEmpty()) {
try {
console(" the queue is empty.");
// 队列是空的,执行提取的线程开始等待
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Event event = eventQueue.removeFirst();
// 通知所有被阻塞的线程,可以开始执行相关操作
this.eventQueue.notifyAll();
console(" the event " + event + " is handled.");
return event;
}
}
private void console(String message) {
System.out.printf("%s:%s\n", currentThread().getName(), message);
}
}
notify和Condition
生产者本来只想通知消费者,但它把其他的生产者也通知了;消费者本来只想通知生产者,但它把其他的消费者通知了。
原因就是wait()和notify()所作用的对象和synchronized所作用的对象是同一个,只能有一个对象,无法区分队列空和列队满两个条件
。这正是Condition
要解决的问题。
Condition
的await
、signal
、singalAll
与Object
的wait
、notify
、notifyAll
都可以实现的需求,两者在使用上也是非常类似,都需要先获取某个锁之后才能调用,而不同的是Object
的wait
、notify
对应的是synchronized
方式的锁,Condition
的await
、singal
则对应的是ReentrantLock
(实现Lock
接口的锁对象)对应的锁。
加入队列已满,所有的生产者现场阻塞,某个时刻消费者消费了一个元素,则需要唤醒某个生产者线程,而通过Object
的notify
方式唤醒的线程不能确保一定就是一个生产者线程,因为notify
是随机唤醒
某一个正在该synchronized
对应的锁上面通过wait
方式阻塞的线程,如果这时正好还有消费者线程也在阻塞中,则很可能唤醒的是一个消费者线程;notifyAll
更是会唤醒所有在对应锁上通过wait
方式阻塞的线程,而不管是生产者还是消费者线程。