多线程协做wait、notify、notifyAll方法简介理解使用 多线程中篇(十四)

在锁与监视器中有对wait和notify以及notifyAll进行了简单介绍
全部对象都有一个与之关联的锁与监视器
wait和notify以及notifyAll之因此是Object的方法就是由于任何一个对象均可以当作锁对象(锁对象也是一种临界资源)
而等待与唤醒自己就是指的临界资源
  • 等待,等待什么?等待获取临界资源
  • 唤醒,唤醒什么?唤醒等待临界资源的线程
因此说,等也好,唤醒也罢,都离不开临界资源,而那个做为锁的Object,就是临界资源
这也是为何必须在同步方法(同步代码块)中使用wait和notify、notifyAll,由于他们必须持有临界资源(锁)的监视器,只有持有了指定锁的监视器,才可以进行相关操做,并且,必须是持有的哪一个锁,才可以在这个锁(临界资源)上进行操做
这个也很容易接受与理解,由于线程的通讯在Java中是针对监视器(锁、临界资源)的,在监视器上的等待与唤醒
你都没持有监视器,你还搞什么?你持有的A监视器,你在B监视器上搞什么?

线程通讯

wait与notify示例
下面的代码示例中,MessageQueue类,有内部有LinkedList,能够用于保存消息,消息为Message
MessageQueue内部个数默认10,能够经过构造函数进行手动设置
提供了生产方法set和获取方法get
若是队列已满,等待,不然生产消息,而且通知消费者获取消息
若是队列已空,等待,不然消费消息,而且通知生产者生产消息
在测试类中开辟两个线程,一个用于生产,一个用于消费(无限循环执行)
package test1;
import java.util.LinkedList;
/**
* 消息队列MessageQueue 测试
*/
public class T13 {
public static void main(String[] args) {
final MessageQueue mq = new MessageQueue(3);
System.out.println("***************task begin***************");
//建立生产者线程并启动
new Thread(() -> {
while (true) {
mq.set(new Message());
}
}, "producer").start();
//建立消费者线程并启动
new Thread(() -> {
while (true) {
mq.get();
}
}, "consumer").start();
}
}
/**
* 消息队列
*/
class MessageQueue {
/**
* 队列最大值
*/
private final int max;
/*
* 锁
* */
private final byte[] lock = new byte[1];
/**
* final确保发布安全
*/
final LinkedList<Message> messageQueue = new LinkedList<>();
/**
* 构造函数默认队列大小为10
*/
public MessageQueue() {
max = 10;
}
/**
* 构造函数设置队列大小
*/
public MessageQueue(int x) {
max = x;
}
public void set(Message message) {
synchronized (lock) {
//若是已经大于队列个数,队列满,进入等待
if (messageQueue.size() > max) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//若是队列未满,生产消息,随后通知lock上的等待线程
//每一次的消息生产,都会通知消费者
System.out.println(Thread.currentThread().getName() + " : add a message");
messageQueue.addLast(message);
lock.notify();
}
}
public void get() {
synchronized (lock) {
//若是队列为空,进入等待,没法获取消息
if (messageQueue.isEmpty()) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//队列非空时,读取消息,随后通知lock上的等待线程
//每一次的消息读取,都会通知生产者
System.out.println(Thread.currentThread().getName() + " : get a message");
messageQueue.removeFirst();
lock.notify();
}
}
}
/**
* 消息队列中存储的消息
*/
class Message {
}
image_5c7c7886_7f2
ps:判断条件 if (messageQueue.size() > max) 因此实际队列空间为4
 
从以上代码示例中能够看得出来,借助于锁lock,实现了生产者和消费者之间的通讯与互斥
他们都是基于这个临界资源进行管理的,这个锁就至关于调度的中心,进入了监视器以后若是条件知足,那么执行,而且会通知其余线程,若是不知足则会等待。
从这个例子中应该能够理解,锁与监视器 和 线程通讯之间的关系

wait方法

image_5c7c7886_2cd0
有三个版本的wait方法,wait,表示在等待此锁(等待持有这个锁对象对应的监视器)
对于无参数的wait以及双参数的wait,能够查看源代码,核心为这个native方法
wait()直接调用wait(0);
wait(long timeout, int nanos)在参数有效性校验后调用wait(timeout)
image_5c7c7886_c15
深刻看下native方法
image_5c7c7886_291b
API解释:
在其余线程调用此对象的 notify() 方法或 notifyAll() 方法或者超过指定的时间量前致使当前线程等待。 
如前面所述,wait以及notify以及notifyAll都须要持有监视器才能够调用该方法
既然另外两个版本都是依赖底层的这个wait,因此全部版本的wait都须要持有监视器
一旦该方法调用,将会进入该监视器的等待集,而且放弃同步要求(也就是再也不持有锁,将会释放锁)
必定注意:将会释放锁,将会释放锁,会释放锁......
image_5c7c7886_743a
除非遇到上面的这几种状况,不然将会线程被禁用,进入休眠状态,也就是持续等待
遇到这几种状况后,将会从对象的等待集中删除线程,并从新进行线程调度
须要注意的是从等待集中删除并不意味着立马执行,他仍旧须要与其余线程竞争,若是竞争失败,也会继续等待
若是一个线程在不止一个锁对象的等待集内,那么将只是解除当前这个锁对象等待集中解锁,在其余等待集中仍旧是锁定的,若是你在多个等待集合中,总不能一会儿就从全部的等待集合中释放,对吧
若是在等待时,任何其余的线程中断了该线程,那么将会收到一个异常,InterruptedException
另外若是没有持有当前监视器,将会抛出异常,IllegalMonitorStateException
 
小结:
对于native方法wait,将会等待指定的时长,若是wait(0),将会持续等待
无参数的wait()就是持续等待
双参数版本的就是等待必定的时长

wait的虚假唤醒

在没有被通知、中断或超时的状况下,线程也可能被唤醒,这被称之为虚假唤醒 (spurious wakeup)
也就是说你没有让他醒来(通知、中断、超时),这彻底是超出你意料的,本身就莫名的醒了
尽管这种事情发生的几率很小,可是仍是应该注意防范
如何防范?
好比咱们上面的生产者方法

 

public void set(Message message) {
synchronized (lock) {
//若是已经大于队列个数,队列满,进入等待
if (messageQueue.size() > max) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//若是队列未满,生产消息,随后通知lock上的等待线程
//每一次的消息生产,都会通知消费者
System.out.println(Thread.currentThread().getName() + " : add a message");
messageQueue.addLast(message);
lock.notify();
}
}
生产者方法中,咱们使用if对条件进行判断
if (messageQueue.size() > max) 
一旦出现虚假唤醒,那么将会从wait方法后面继续执行,也就是下面的
      messageQueue.addLast(message);
      lock.notify();
很显然,虚假唤醒的时候,条件极可能是仍旧不知足的,继续生产,岂不出错?
因此咱们应该唤醒后再次的进行条件判断,如何进行?
能够把if条件判断换成while条件测试,这样即便唤醒了也会再次的确认是否条件知足,若是不知足那么确定会继续进入等待,而不会继续往下执行
小结:
咱们应该老是使用循环测试条件来确保条件的确知足,避免小几率发生的虚假唤醒问题

notify方法

image_5c7c7886_7439
notify也是一个本地方法,他将会唤醒在该监视器上等待的某个线程(关键词:当前监视器、某一个线程)
即便在该监视器上有多个线程正在等待,那么也是仅仅唤醒一个
并且,选择是任意的
另外还须要注意,是这边notify以后,那么马上就有什么反应了吗?不是的!
只有当前持有监视器的线程执行结束,才有机会执行被唤醒的线程,并且被唤醒的线程仍旧须要参与竞争(若是入口集中还有线程在等待的话)
因此,若是一个1000行的方法,无论你在哪一行执行notify,终归是要方法结束后,被唤醒的线程才有机会
notify问题
notify仅仅唤醒其中一个线程,并且,这种机制是非公平的,也就是说不可以保障每一个线程必然都有机会得到执行。
换个说法,好比10个小朋友等待老师发糖果,若是每次都随机选一个,可能有的小朋友一直都得不到糖果
这就会发生线程的饥饿
怎么解决?
咱们还有notifyAll方法,与notify功能相同,可是差异在于将会唤醒全部等待线程,这样全部的等待集合都得到了一次重生的机会,固然,若是条件不知足可能继续进入等待集,若是没有竞争成功也会在入口集等待
经过notifyAll能够确保没有人会饿到

notifyAll方法

image_5c7c7886_3858
这也是一个本地方法,看得出来,无论等待仍是通知,最终仍旧须要借助于JVM底层。经过操做系统来实现
notifyAll唤醒在此对象监视器上等待的全部线程
与notify除了唤醒线程个数区别外,无任何区别,仍旧是执行结束后,被唤醒的线程才有机会

多线程通讯

借助于wait与notify能够完成线程间的通讯,能够借助于wait和notifyAll完成多线程之间的通讯
其实对于咱们最上面的代码示例中,不只仅虚假唤醒会出现问题,非虚假唤醒场景下也可能出现问题
在只有一个生产者和消费者时并不会出现问题,可是若是在更多线程场景下,就可能出现问题
 
好比,两个生产者A,和B,一个消费者C,执行一段时间后,假设此时队列已满
若是A执行时,发现已满,进入等待
而后B线程执行,仍旧是已满,进入等待
而后C线程开始执行,消费了一个消息后,调用notify,此时碰巧唤醒了线程A
线程C执行后,线程A竞争成功,进入同步区域执行,线程A生产了一个消息,而后调用notify 
不巧的是,此时唤醒的是线程B,线程B醒来之后竞争成功,继续执行,因而继续往队列中添加,也就是调用addLast方法
很显然,出问题了,出现了已满可是仍旧调用addLast方法
这种场景下,问题出如今唤醒了一个线程后,其实条件仍旧不知足,好比上面的描述中,应该唤醒消费者,可是生产者却被唤醒了,并且此时条件并不知足
 
一样的道理,若是是队列已经空了,假设有两个消费者线程A,B,和一个生产者C
消费者A,发现空,wait
消费者B,发现空,wait
生产者C,生产一个消息,notify,唤醒A
A醒来后竞争成功,消费一个消息后,notify,唤醒了B
B醒来后竞争成功,将会继续消费消息,出现已经空了,可是仍旧会调用removeFirst方法
 
从结果看,跟虚假唤醒是相似的---醒来时,条件仍旧不知足
因此解决方法就是将if条件判断修改成while条件检测
从这一点也能够看得出来,咱们应该老是使用while对条件进行检测,不只能够避免虚假唤醒,也可以避免更多线程并发时的同步问题
 
若是咱们使用了while进行条件检测
假如说有10个生产者,队列大小为5,一个消费者
碰巧刚开始是10个生产者运行,接着队列已满,10个线程都进入wait状态
碰巧接下来是消费者不断消费,持续消费了5个消息,唤醒了其中5个生产者,而后进入wait
若是接下来是这五个生产者唤醒的线程都是刚才进入wait的生产者,会发生什么?
最终全部的生产者都将进入wait状态!而那个消费者也仍旧是wait!全部的人都在wait,谁来解锁?
 
这其中的一个问题就是咱们不知道notify将会唤醒哪一个线程,有些场景将会致使消费者永远没法得到执行的机会
因此应该使用notifyAll,这样将保障消费者始终有机会执行,哪怕暂时没机会执行,他仍旧是醒着的,只要她醒着就有机会让整个车间动起来
 
以下图所示,将原来的MessageQueue中的重构为RefactorMessageQueue,其实仅仅修改if为while
测试方法中,队列设置为5(代码中使用>判断,因此实际是6),生产者设置为20个,能够看到很快就死锁了,而且给线程设置名称
image_5c7c7886_2719
 
***************task begin***************
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : queue is full ,waiting...
producer1 : queue is full ,waiting...
producer2 : queue is full ,waiting...
producer3 : queue is full ,waiting...
producer4 : queue is full ,waiting...
producer5 : queue is full ,waiting...
producer6 : queue is full ,waiting...
producer7 : queue is full ,waiting...
producer8 : queue is full ,waiting...
producer9 : queue is full ,waiting...
producer10 : queue is full ,waiting...
producer11 : queue is full ,waiting...
producer12 : queue is full ,waiting...
producer13 : queue is full ,waiting...
producer14 : queue is full ,waiting...
producer15 : queue is full ,waiting...
producer16 : queue is full ,waiting...
producer17 : queue is full ,waiting...
producer18 : queue is full ,waiting...
producer19 : queue is full ,waiting...
consumer : get a message
consumer : get a message
consumer : get a message
consumer : get a message
consumer : get a message
consumer : get a message
consumer : queue is empty ,waiting...
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : add a message
producer0 : queue is full ,waiting...
producer6 : queue is full ,waiting...
producer11 : queue is full ,waiting...
producer10 : queue is full ,waiting...
producer9 : queue is full ,waiting...
producer8 : queue is full ,waiting...
producer7 : queue is full ,waiting...
producer5 : queue is full ,waiting...
producer4 : queue is full ,waiting...
producer3 : queue is full ,waiting...
producer2 : queue is full ,waiting...
producer1 : queue is full ,waiting...
 
关键部分,以下图,消费者wait后,紧接着生产者满了,而后就纷纷wait
image_5c7c7887_5328
能够经过Jconsole工具查看
这是官方提供的工具,本地安装配置过JDK后,能够命令行直接输入:jconsole便可,而后会打开一个界面窗口
  1. 命令行输入jconsole
  2. 选择进程,链接
  3. 点击线程查看
image_5c7c7887_28f0
逐个查看一下每一个线程的状态,你会发现,咱们的20个生产者producerX(0-19)以及一个消费者consumer,所有都是:状态: [B@2368a10b上的WAITING
image_5c7c7887_10ee
小结:
多线程场景下,应该老是使用while进行循环条件检测,而且老是使用notifyAll,而不是notify,以免出现奇怪的线程问题

总结

wait、notify、notifyAll方法,都须要持有监视器才可以进行操做,而进入监视器也就是须要在synchronized方法或者代码块内,或者借助于显式锁同步的代码块内
wait的方法签名中,能够看到将会可能抛出InterruptedException,说明wait是一个可中断的方法,当其余线程对他进行中断后(调用interrupt方法)将会抛出异常,而且中断状态将会被擦除,被中断后,该线程至关于被唤醒了  
鉴于notify场景下的种种问题,咱们应该尽量的使用notifyAll
相关文章
相关标签/搜索