wait放弃对象锁_wait、notify、notifyAll原始同步用具详解!中篇[十四]

点击上方 “ 布衣码农 ” ,免费订阅~选择“ 设为星标 ”,第一时间免费获得更新~

e61c3ca1580266b6419341c3588271ac.png「布衣码农」对于理解同步机制有重大意义。 在锁与监视器中有对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 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 {}
e7db44cba1f3b883e9fa4b2f1280c7c6.png

ps:

判断条件 if (messageQueue.size() > max) 所以实际队列空间为4

从以上代码示例中可以看得出来: 借助于锁lock,实现了生产者和消费者之间的通信与互斥。 他们都是基于这个临界资源进行管理的,这个锁就相当于调度的中心。 进入了监视器之后如果条件满足,那么执行,并且会通知其他线程; 如果不满足则会等待; 从这个例子中应该可以理解,锁与监视器 和 线程通信之间的关系。

wait方法

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

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

notifyAll方法

0409310d3dfa1d3841a6f5981bfaa3ae.png 这也是一个本地方法。 看得出来,不管等待还是通知,最终仍旧需要借助于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个,可以看到很快就死锁了,并且给线程设置名称。 27fbb08df64d4e97bde539f49a8c0f99.png
***************task begin***************producer0 : add a messageproducer0 : add a messageproducer0 : add a messageproducer0 : add a messageproducer0 : add a messageproducer0 : add a messageproducer0 : 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 messageconsumer : get a messageconsumer : get a messageconsumer : get a messageconsumer : get a messageconsumer : get a messageconsumer : queue is empty ,waiting...producer0 : add a messageproducer0 : add a messageproducer0 : add a messageproducer0 : add a messageproducer0 : add a messageproducer0 : add a messageproducer0 : 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 182bb8561bd6352c0ca734c2ce4473ab.png 可以通过Jconsole工具查看 这是官方提供的工具,本地安装配置过JDK后,可以命令行直接输入:jconsole即可,然后会打开一个界面窗口
  1. 命令行输入jconsole

  2. 选择进程,连接

  3. 点击线程查看

92971285d683e94087f53d94aadf9f7f.png 逐个查看一下每个线程的状态,你会发现,我们的20个生产者producerX(0-19)以及一个消费者consumer,全部都是:
状态: [B@2368a10b上的WAITING
9696b5bb2a113c805fcbbb4df51609ad.png 小结: 多线程场景下,应该总是使用while进行循环条件检测; 并且总是使用notifyAll,而不是notify,以避免出现奇怪的线程问题。

总结

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

··················END··················

注:非技术讲解配图均来源于网络

期待分享

如果对你有用

可以点个 「在看」 或者分享到 「 朋友圈 」 哦

08ace965c37f9b9ee90d8cb620e9f3d5.png你「在看」吗? c9e33fd18786b3d002eeec44d390c528.gif↓↓

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值