Java并发编程(五)---线程通信

举报
码农飞哥 发表于 2021/05/29 13:02:40 2021/05/29
【摘要】 文章首发于:https://mp.weixin.qq.com/s/sbkY-Il1AQ0Ew-b7LKc33g 前言 上一篇我们介绍了死锁的发生条件,以及避免死锁的方式。其中 破坏占有且等待的处理是,通过一个单例类一次性申请所有资源,直到成功。如while (!Allocator.getAllocator().applyResource(this, target))...

文章首发于:https://mp.weixin.qq.com/s/sbkY-Il1AQ0Ew-b7LKc33g

前言

上一篇我们介绍了死锁的发生条件,以及避免死锁的方式。其中 破坏占有且等待的处理是,通过一个单例类一次性申请所有资源,直到成功。如while (!Allocator.getAllocator().applyResource(this, target)) { return; } 如果在并发量比较小的情况下,还可以接受,如果并发量比较大的话,就会大量的消耗CPU的资源。这时候,我们应该引入线程通信,主要是 等待-唤醒机制。

线程通信

下面我们通过一个例子来说明。场景说明:
图书馆里,有一本书叫《Java 高并发实战》,小A早上的时候把这本书借走了,小B中午的时候去图书馆找这本书,这里小A和小B分别是两个线程,他们都要看的书是共享资源。

通过共享对象通信

小B去了图书馆,发现这本书被借走了,他回到家,等了几天,再去图书馆找这本书,发现这本书已经被归还了,他顺利借走了书。
用程序模拟的话,就是线程小A调用setCanBorrow方法将canBorrow设为true,线程小B调用getCanBorrow获取。在这里线程A和B必须获得
指向同一个LibraryTest1共享实例的引用,如果持有的对象指向不同的LibraryTest1实例,那么彼此将不能检测到对方的信号。

public class LibraryTest1 {
	//是否可借 private boolean canBorrow = false; synchronized boolean getCanBorrow() { return canBorrow; } synchronized void setCanBorrow(boolean canBorrow) { this.canBorrow = canBorrow; }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

忙等待

其实小A在小B走后一会就把书还回去了,小B却在几天后才去找书,为了早点借到书(减少延迟),小B可能在图书馆等着,每隔几分钟(while循环),他就去检查这本书有没有被还回,这样只要小A一还回书,小B马上就会知道。

public class LibraryTest1 { final LibraryTest1 libraryTest1 = new LibraryTest1(); while (!libraryTest1.getCanBorrow()) { //空等 return; }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

wait(),notify()和notifyAll()

很多次后,小B发现自己这样做太累了,身体有点吃不消,不过很快,学校图书馆系统改进,加入了短信通知功能(notify()),只要小A一还回书,图书馆会立马通知小B,这样小B就可以在家睡觉等短信了。

//是否可借
  private boolean canBorrow = false; synchronized String borrowBook() throws InterruptedException { if (!canBorrow) { wait(); return null; }
		canBorrow = false; return "Java 高并发实战"; } synchronized void giveBackBook() { this.canBorrow = true; notify(); }

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如上程序,canBorrow 初始为false时(书本已经被小A借走),小A 还书(调用giveBackBook方法)之后,书的可借状态为可借,并且调用notify()通知等待线程(系统发短信给小B)需要注意的两点是:

  1. wait(),notify() 和或者notifyAll() 都需要在同步代码块中调用(就是消息只能由图书管理系统发出),不能再同步代码块之外调用,否则,会抛出IllegalMonitorStateException异常。
  2. notify() 是随机的通知等待队列里的某一个线程,而notifyAll()是通知等待队列里的所有线程。一般而言最好调用notifyAll(),而不要调用notify()。因为,通知时,只是表示通知时间点条件满足,等线程执行时,条件可能已经不满足了,线程的执行时间与通知时间不重合,如果调用notify()的话很快能不能通知到我们期望通知的线程。

wait()与sleep() 的相同点与不同点

相同点:都会让当前线程挂起一段时间,让渡CPU的执行时间,等待再次调度
不同点:

  1. wait(),notify(),notifyAll()一定是在synchronized{}内部调用,等待和通知的对象必须要对应。而sleep可以再任何地方调用
  2. wait 会释放锁对象的“锁标志”,当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中。知道调用notifyAll()方法,而sleep则不会释放,也就是说在休眠期间,其他线程仍然不能访问共享数据。
  3. wait可以被唤醒,sleep的只能等其睡眠结束
  4. wait()是在Object 类里,而sleep是在Thread 里的

丢失的信号

学校图书馆系统是这么设计的,当一本书被还回来的时候,会给等待着发送短信,并且只会发一次,如果没有等待者,他也会发(只不过没有接受者)。问题出现了,因为短信只会发一次,当书被还回来的时候,没有人等待借书,他会发一条空短信,但是之后有等待借此本书的同学永远也不会再收到短信,导致这些同学会无休止的等待,为了避免这个问题,我们等待的时候先打个电话问问图书管理员是否继续等待if(!wasSignalled)

public class LibraryTest3 { private MonitorObject monitorObject = new MonitorObject(); private boolean canBorrow = false; private boolean wasSignalled = false; String borrowBook() throws InterruptedException { synchronized (monitorObject) { if (!canBorrow||!wasSignalled) { wait(); return null; } canBorrow = false; return "Java 高并发实战"; } } void giveBackBook() { synchronized (monitorObject) { this.canBorrow = true; this.wasSignalled = true; notifyAll(); } }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

notify()和notifyAll()方法不会保存调用它们的方法,因为当这两个方法被调用时,有可能没有线程处于等待状态,通知信号过后便丢弃了。因此,如果一个线程先于被通知线程调用wait()前调用notify(),等待的线程就将错过这个信号。在某些情况下,这可能使得等待线程永久等待,不再被唤醒。所以,为了避免丢失信号,我们需要将信号保存到信号类里。这里的管理员就是信号类。

假唤醒

图书馆系统还有一个BUG:系统会偶尔给你发条错误短信,说书可以借了(其实不可以借),我们之前已经该图书馆管理员打过电话了,他说让我们等短信。我们很听话,一等到短信(其实是bug引起的错误短信),就去借书了,到了图书馆后发现这书根本没有还回来!我们很郁闷,但也没有办法呀,学校不修复BUG,我们得聪明点:每次在收到短信后,再打电话问问书到底能不能借while(!canBorrow||!wasSignalled)

 String borrowBook() throws InterruptedException { synchronized (monitorObject) { while (!canBorrow||!wasSignalled) { wait(); return null; } canBorrow = false; return "Java 高并发实战"; } }

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

就像案例里说的,有时候线程会被莫名奇妙的唤醒,为了防止假唤醒,保存信号的成员变量将在一个while循环里接收检查。而不是在if表达式里。

不要对常量字符串或全局对象调用wait()

因为如果使用常量字符串的话。JVM/编译器内部会把常量字符串转成同一个对象,
这意味着即使你有2个不同的MyWaitNotify3实例,它们都是引用了相同的空字符串实例。同时也意味着存在这样的风险,在第一个MyWaitNotify3实例调用doWait()会被第二个MyWaitNotify3实例上调用doNotify()的线程唤醒。

public class MyWaitNotify3 { String monitorObject = ""; boolean isSignaled = false; public void doWait() { synchronized (monitorObject) { while (!isSignaled) { try { monitorObject.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //清楚标记,继续执行 isSignaled = false; } } public void doNotify() { synchronized (monitorObject) { isSignaled = true; monitorObject.notifyAll(); } }

}


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

总结

本文主要介绍了线程之间的通信,通过一个现实中的场景再现了这5种场景,其中等待-通知机制是线程通信的核心机制。工作中用轮询的方式来等待某个状态,很多情况下都可以用等待-通知机制。
参考资料:
线程通信

文章来源: feige.blog.csdn.net,作者:码农飞哥,版权归原作者所有,如需转载,请联系作者。

原文链接:feige.blog.csdn.net/article/details/102864295

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。