在锁与监视器中有对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 {
}
ps:判断条件 if (messageQueue.size() > max) 所以实际队列空间为4
 
从以上代码示例中可以看得出来,借助于锁lock,实现了生产者和消费者之间的通信与互斥
他们都是基于这个临界资源进行管理的,这个锁就相当于调度的中心,进入了监视器之后如果条件满足,那么执行,并且会通知其他线程,如果不满足则会等待。
从这个例子中应该可以理解,锁与监视器 和 线程通信之间的关系

wait方法

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

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

notifyAll方法

这也是一个本地方法,看得出来,不管等待还是通知,最终仍旧需要借助于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个,可以看到很快就死锁了,并且给线程设置名称
 
***************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
可以通过Jconsole工具查看
这是官方提供的工具,本地安装配置过JDK后,可以命令行直接输入:jconsole即可,然后会打开一个界面窗口
  1. 命令行输入jconsole
  2. 选择进程,连接
  3. 点击线程查看
逐个查看一下每个线程的状态,你会发现,我们的20个生产者producerX(0-19)以及一个消费者consumer,全部都是:状态: [B@2368a10b上的WAITING
小结:
多线程场景下,应该总是使用while进行循环条件检测,并且总是使用notifyAll,而不是notify,以避免出现奇怪的线程问题

总结

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

多线程协作wait、notify、notifyAll方法简介理解使用 多线程中篇(十四)的更多相关文章

  1. Java多线程_wait/notify/notifyAll方法

    关于这三个方法,我们可以查询API得到下列解释: wait():导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法或者指定的事件用完 notify( ...

  2. Java多线程:wait(),notify(),notifyAll()

    1. wait(),notify(),notifyAll() 2. wait() 2.1. wait() 2.2. wait(long timeout) 2.3. wait(long timeout, ...

  3. 为什么 wait()方法和 notify()/notifyAll()方法要在同步块 中被调用 ?

    这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对 象的锁

  4. Java多线程的wait(),notify(),notifyAll()

    在多线程的情况下.因为多个线程与存储空间共享相同的过程,同时带来的便利.它也带来了访问冲突这个严重的问题. Java语言提供了一种特殊的机制来解决这类冲突,避免同一数据对象由多个线程在同一时间访问. ...

  5. 进程间协作---wait,notify,notifyAll

    转自牛客网的一篇评论,解释的十分详细 在 Java 中,可以通过配合调用 Object 对象的 wait() 方法和 notify()方法或 notifyAll() 方法来实现线程间的通信.在线程中调 ...

  6. java 多线程(wait/notify/notifyall)

    package com.example; public class App { /* wait\notify\notifyAll 都属于object的内置方法 * wait: 持有该对象的线程把该对象 ...

  7. 多线程-4.wait() notify() notifyAll() 生产者消费者模型

    1.wait()方法 该方法继承于Object类.在调用obj.wait()方法后,当前线程会失去obj的锁.待其他线程调用obj.notify()或notifyAll()方法后进入锁等待池,争抢到锁 ...

  8. 深入理解java虚拟机(十四)正确利用 JVM 的方法内联

    在IntelliJ IDEA里面Ctrl+Alt+M用来拆分方法.选中一段代码,敲下这个组合,非常简单.Eclipse也用类似的快捷键,使用 Alt+Shift+M.我讨厌长的方法,提起这个下面这个方 ...

  9. 零基础学习java------day18------properties集合,多线程(线程和进程,多线程的实现,线程中的方法,线程的声明周期,线程安全问题,wait/notify.notifyAll,死锁,线程池),

    1.Properties集合 1.1 概述: Properties类表示了一个持久的属性集.Properties可保存在流中或从流中加载.属性列表中每个键及其对应值都是一个字符串 一个属性列表可包含另 ...

随机推荐

  1. CentOS DesktopEntry

    IBM Developer  :  https://www.ibm.com/developerworks/cn/linux/l-cn-dtef/index.html [Desktop Entry] N ...

  2. 搞定! iTunes 不能添加铃声进去

    最近换个新铃声,但转换成.m4r 怎么都拖不到铃声里很莫名奇妙,首先确定苹果是允许自己定义铃声的,然后网上查了不少文章,都无济于事所以我想自己记录下自己成功搞定的方法,供各位参考第一步: 把自己想转为 ...

  3. Java的序列化和反序列化

    概述 Java对象的序列化和反序列化,这个词对我来说追溯到大学阶段,学Java对象流时知道有这东西.老师告诉我们可以把Java对象化作字节流,储存文件或网络通信.然后就是巴啦巴拉,一脸懵逼.举个例子, ...

  4. Docker 集群

    1.  理解swarm swarm(译:集群) 一个swarm是一组运行着Docker的机器,它们一起加入到一个集群.swarm中的机器既可以是物理机,也可以是虚拟机.在加入到一个swarm后,每台机 ...

  5. Python练习:哥德巴赫猜想

    哥德巴赫猜想 哥德巴赫 1742 年给欧拉的信中哥德巴赫提出了以下猜想:任一大于 2 的偶数都可写成两个质数之和.但是哥德巴赫自己无法证明它,于是就写信请教赫赫有名的大数学家欧拉帮忙证明,但是一直到死 ...

  6. Nginx 配置 Https 免费证书访问

    配置HTTPS 现在做博客或者做网站没有 https 已经不行了,就记录一下我在腾讯云配置 https 的过程吧,非常简单,1个小时就可以了. 还涉及到 http 访问自动转发到 https 访问路径 ...

  7. asp.net core系列 48 Identity 身份模型自定义

    一.概述 ASP.NET Core Identity提供了一个框架,用于管理和存储在 ASP.NET Core 应用中的用户帐户. Identity添加到项目时单个用户帐户选择作为身份验证机制. 默认 ...

  8. 你真的了解字典(Dictionary)吗?

    从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点. 为了便于描述,我把上面的那条线路称为线路1,下面的称为线路2. 思路 ...

  9. 委托与lambda关系

    什么是委托委托是没有方法体的,声明委托就是一个关键字: delegate ,委托可以试有参无参,有返回值无返回值.和我们的方法是一样的.不同的区别是 委托没有方法体的,委托可放在类下也可以放在类的外面 ...

  10. Rest_framework Serializer 序列化 (含源码浅解序列化过程)

    目录 Rest_framework Serializer 序列化 序列化与反序列化中不得不说的感情纠葛 三角恋之 save/update/create 四角恋之 序列化参数instance/data/ ...