Java深入学习29:线程等待和唤醒的两个方案

模拟场景

一个门店,有一个店员,有消费者来消费商品(每次消费1件商品),有仓库人员来添加(生产)商品(每次生产1件商品),并假设库存上限是2.

基础代码实现

public classThreadNotifyTest {public static voidmain(String[] args) {

Clerk clerk= newClerk();

Producer producer= newProducer(clerk);

Consumer consumer= newConsumer(clerk);new Thread(producer,"生产者A").start();new Thread(consumer,"消费者B").start();new Thread(producer,"生产者C").start();new Thread(consumer,"消费者D").start();

}

}classClerk{private int proNum = 0;

get(){

......//参考具体方案

}

add(){

......//参考具体方案

}

}class Producer implementsRunnable{privateClerk clerk;publicProducer(Clerk clerk) {this.clerk =clerk;

}

@Overridepublic voidrun() {for(int i=0; i<10; i++){

clerk.add();

}

}

}class Consumer implementsRunnable{privateClerk clerk;publicConsumer(Clerk clerk) {this.clerk =clerk;

}

@Overridepublic voidrun() {for(int i=0; i<10; i++){

clerk.get();

}

}

}

方案1:基础版本。使用synchronized ,get 时判断是否缺货,add 时判断是否库满

问题:根据结果发现,在生产、消费过程中会出现大量的库满和缺货情况。

classClerk{private int proNum = 0;public synchronized voidget(){if(proNum <= 0){

System.out.println("缺货");

}else{

System.out.println(Thread.currentThread().getName()+ ": " + --proNum);

}

}public synchronized voidadd(){if(proNum > 1){

System.out.println("库满");

}else{

System.out.println(Thread.currentThread().getName()+ ": " + ++proNum);

}

}

}

方案2:使用 wait 和 notifyAll 方法,解决方案1问题;但是引发了新问题

问题:程序会出现无法终止的情况。原因在于当循环操作在进行到最后时,会出现一个线程一直在等待,没有其他线程唤醒该等待线程。

classClerk{private int proNum = 0;public synchronized voidget(){if(proNum <= 0){

System.out.println("缺货");try{this.wait();

}catch(InterruptedException e) {

e.printStackTrace();

}

}else{

System.out.println(Thread.currentThread().getName()+ ": " + --proNum);try{this.notifyAll();

}catch(Exception e) {

e.printStackTrace();

}

}

}public synchronized voidadd(){if(proNum > 1){

System.out.println("库满");try{this.wait();

}catch(InterruptedException e) {

e.printStackTrace();

}

}else{try{this.notifyAll();

}catch(Exception e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()+ ": " + ++proNum);

}

}

}

方案3:我们对get和add方法内部进行了优化,去掉了if-else中的else;解决了线程一直等待问题;但同时引入了新的问题;

问题:库存会溢出或者为负数;原因是多个生产者和消费者修改库存,产生了脏数据(比如两个生产者同时被欢迎,执行了++proNum,导致库存溢出);这也是常说的虚假唤醒情况。Object类中的wait()方法也专门针对该情况做了推荐处理(参考方案4)。

class Clerk{

private int proNum = 0;

public synchronized void get(){

if(proNum <= 0){

System.out.println("缺货");

try {

this.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

System.out.println(Thread.currentThread().getName() + ": " + --proNum);

try {

this.notifyAll();

} catch (Exception e) {

e.printStackTrace();

}

}

public synchronized void add(){

if(proNum > 1){

System.out.println("库满");

try {

this.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

try {

this.notifyAll();

} catch (Exception e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + ": " + ++proNum);

}

}

方案4:使用 while 替换 if 判断,解决虚假唤醒问题

classClerk{private int proNum = 0;public synchronized voidget(){while(proNum <= 0){

System.out.println("缺货");try{this.wait();

}catch(InterruptedException e) {

e.printStackTrace();

}

}

System.out.println(Thread.currentThread().getName()+ ": " + --proNum);try{this.notifyAll();

}catch(Exception e) {

e.printStackTrace();

}

}public synchronized voidadd(){while(proNum > 1){

System.out.println("库满");try{this.wait();

}catch(InterruptedException e) {

e.printStackTrace();

}

}try{this.notifyAll();

}catch(Exception e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()+ ": " + ++proNum);

}

}

方案5:使用 Lock 锁和 Condition

classClerk{private int proNum = 0;private Lock lock = newReentrantLock();private Condition condition =lock.newCondition();public voidget(){

lock.lock();try{while(proNum <= 0){

System.out.println("缺货");try{

condition.await();

}catch(InterruptedException e) {

e.printStackTrace();

}

}

System.out.println(Thread.currentThread().getName()+ ": " + --proNum);try{

condition.signalAll();

}catch(Exception e) {

e.printStackTrace();

}

}finally{

lock.unlock();

}

}public voidadd(){

lock.lock();try{while(proNum > 1){

System.out.println("库满");try{

condition.await();

}catch(InterruptedException e) {

e.printStackTrace();

}

}try{

condition.signalAll();

}catch(Exception e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName()+ ": " + ++proNum);

}finally{

lock.unlock();

}

}

}

总结

1- 方案4和方案5,针对模拟场景,是比较合理的方案。

几个概念:

1- Java中对象锁的模型

java中对象锁的模型,JVM会为一个使用内部锁(synchronized)的对象维护两个集合,Entry Set和Wait Set,也有人翻译为锁池和等待池,意思基本一致。对于Entry Set:如果线程A已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。对于Wait Set:如果线程A调用了wait()方法,那么线程A会释放该对象的锁,进入到Wait Set,并且处于线程的WAITING状态。还有需要注意的是,某个线程B想要获得对象锁,一般情况下有两个先决条件,一是对象锁已经被释放了(如曾经持有锁的前任线程A执行完了synchronized代码块或者调用了wait()方法等等),二是线程B已处于RUNNABLE状态。那么这两类集合中的线程都是在什么条件下可以转变为RUNNABLE呢?对于Entry Set中的线程,当对象锁被释放的时候,JVM会唤醒处于Entry Set中的某一个线程,这个线程的状态就从BLOCKED转变为RUNNABLE。对于Wait Set中的线程,当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某一个线程,这个线程的状态就从WAITING转变为RUNNABLE;或者当notifyAll()方法被调用时,Wait Set中的全部线程会转变为RUNNABLE状态。所有Wait Set中被唤醒的线程会被转移到Entry Set中。然后,每当对象的锁被释放后,那些所有处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程(具体哪一个取决于JVM实现,队列里的第一个?随机的一个?)真正获取到对象的锁,而其他竞争失败的线程继续在Entry Set中等待下一次机会。

2- wait()方法外面为什么是while循环而不是if判断

我们在调用wait()方法的时候,心里想的肯定是因为当前方法不满足我们指定的条件,因此执行这个方法的线程需要等待直到其他线程改变了这个条件并且做出了通知。那么为什么要把wait()方法放在循环而不是if判断里呢,其实答案显而易见,因为wait()的线程永远不能确定其他线程会在什么状态下notify(),所以必须在被唤醒、抢占到锁并且从wait()方法退出的时候再次进行指定条件的判断,以决定是满足条件往下执行呢还是不满足条件再次wait()呢。

3- notifyAll() 和 notify() 区别

notify()是唤醒一个线程,notifyAll()是唤醒全部线程。notify()非常容易导致死锁,

附录1-方案日志

END

java队列等待唤醒_Java深入学习29:线程等待和唤醒的两个方案相关推荐

  1. java同步锁售票_Java基础学习笔记: 多线程,线程池,同步锁(Lock,synchronized )(Thread类,ExecutorService ,Future类)(卖火车票案例)...

    学习多线程之前,我们先要了解几个关于多线程有关的概念. 进程:进程指正在运行的程序.确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能. 线程:线程是 ...

  2. java aqs实现原理_JAVA基础学习之-AQS的实现原理分析

    AbstractQueuedSynchronizer是JUC的核心框架,其设计非常精妙. 使用了 Java 的模板方法模式. 首先试图还原一下其使用场景: 对于排他锁,在同一时刻,N个线程只有1个线程 ...

  3. java 队列已满_JAVA中常见的阻塞队列详解

    在之前的线程池的介绍中我们看到了很多阻塞队列,这篇文章我们主要来说说阻塞队列的事. 阻塞队列也就是 BlockingQueue ,这个类是一个接 口,同时继承了 Queue 接口,这两个接口都是在JD ...

  4. java 怎么写异步方法_java如何学习异步编程?

    昨天头儿给的学习文档我还没看完,头儿说:"MongoDB光会简单的添删改查什么的不行,要深入了解,你们连$set和$inc使用场景都分不清." 确实,学习过一年多SQL,确实对学习 ...

  5. java类的心得_java面向对象学习心得3篇

    日记网 >> 专题 java面向对象学习心得3篇 更新时间:2018/6/15 8:27:00  点击率:937  手机版 java面向对象学习心得3篇来自简单日记网精选推荐.在面向对象的 ...

  6. java 队列和堆栈_Java中的堆栈和队列

    java 队列和堆栈 我最近一直在研究一些需要堆栈和队列的Java代码. 使用的选择不是立即显而易见的. 有一个Queue接口,但没有明确的具体实现要使用. 还有一个Stack类,但是javadocs ...

  7. java condition详解_Java使用Condition控制线程通信的方法实例详解

    Java使用Condition控制线程通信的方法实例详解 发布于 2020-4-20| 复制链接 摘记: 本文实例讲述了Java使用Condition控制线程通信的方法.分享给大家供大家参考,具体如下 ...

  8. java开发保险案例_Java实现双保险线程的示例代码

    双保险线程,每次启动2个相同的线程,互相检测,避免线程死锁造成影响. 两个线程都运行,但只有一个线程执行业务,但都会检测对方的时间戳 如果时间戳超过休眠时间3倍没有更新的话,则重新启动对方线程. 例子 ...

  9. java中的让步_java基本教程之线程让步 java多线程教程

    本章涉及到的内容包括: 1. yield()介绍 2. yield()示例 3. yield() 与 wait()的比较 1. yield()介绍 yield()的作用是让步.它能让当前线程由&quo ...

最新文章

  1. RStudio v1.2.1335 发布,R 语言的集成开发环境
  2. MySQL中的联合索引学习教程
  3. linux alsa 录音程序,Linux下alsa直接录音代码
  4. 【Python】Elasticsearch和elasticsearch_dsl
  5. C++11实现自旋锁
  6. list下界_下界理论
  7. 单点登录(Single Sign On)
  8. pnp型三极管 饱和 截至_截至2013年核心Java帖子
  9. 数据线CE测试标准 准备资料
  10. umijs 隐藏开发工具_使用UmiJS框架开发React应用
  11. IT6302 电源后面板的 DB9 接口输出为 TTL 电平,您需要通过附件电平转换后才可连接到 PC 机的串口上
  12. PHP(阿里云短信验证码)
  13. C++ QT中国象棋项目讲解(三) 单机双人对战走棋
  14. 从“NVIDIA禁令”看如何正确选择NVIDIA GPU卡
  15. 用Python自动化办公操作PPT,掌握这些技巧没压力!
  16. 注塑模具设计师要懂得的四个概念
  17. 光纤布拉格光栅(FBG)笔记【1】:波导结构和布拉格波长推导
  18. ubuntu中把软件放在桌面
  19. 电磁学3——电磁波的频谱与应用
  20. 应用性能测试关注点(来自听云)

热门文章

  1. how is object structure really created - when the child equipment is downloaded
  2. Middleware trace tool SMWT
  3. RFC Destination WORKFLOW_LOCAL_001 - User WF-BATCH
  4. 如何得到当前application server的host name和port number
  5. 如何分析SAP CRM UI label显示成technical name的问题
  6. Spring MVC原理学习之how is return type handled
  7. 502 Bad Gateway Registered endpoint failed to handle the request
  8. 在Windows笔记本上调试运行在iOS设备上的前端应用
  9. oracle 游标循环 while,Oracle的游标使用方法 三种循环
  10. es查询index生成时间_Elasticsearch开始的第一步索引index