线程通信的目的是让线程之间能够相互发送信号.更可能是可以让线程去等待其余线程的信号.如线程B等待线程A的信号用于指示数据已经准备就绪等待处理.html
一个让线程通信的简单方法是经过共享对象来设置信号量.线程A在同步代码块中设置布尔类型的成员变量hasDataToProcess
为true
.线程B一样经过同步代码块来读取成员变量hasDataToProcess
.下面是一个简单的例子,让一个对象包含一个信号量,并提供相应的设置和检查方法.java
public class MySignal {
private boolean hasDataToProcess = false;
public boolean isHasDataToProcess(){
return this.hasDataToProcess;
}
public void setHasDataToProcess(boolean hasDataToProcess){
this.hasDataToProcess = hasDataToProcess;
}
}
复制代码
线程A和B须要有一个指向同一个对象的引用,才能让信号量正常工做.若是线程A和B引用的不是同一个对象,那么信号量将没法正常工做.待处理数据可以独立于信号量存储在共享缓冲区中.post
线程B处理数据前须要等待数据进入就绪状态,即等待线程A将信号量hasDataToProcess
置换为true
.所以线程B须要在循环中等待信号量:性能
private MySignal sharedSignal= ...
...
while(!sharedSignal.hasDataToProcess()){
// 繁忙的等待
}
复制代码
须要注意的是,当信号量不为true
, 循环会一直进行.咱们称这种状况为繁忙的等待.线程一直在循环等待.this
若是平均等待时间相对较长,对于计算机cpu来讲,繁忙等待并非一个高效的方式.所以让线程在等待信号的过程当中进入睡眠或闲置状态是一个不错的选择.spa
Java有一套内建的等待机制,用于让线程在等待信号时进入闲置状态.java.lang.Object
声明了三个方法, wait()
notify()
和 notifyAll()
用于支持这套机制.线程
一个线程能够调用任一对象的wait()
方法来进入闲置状态, 其余对象能够调用相同对象的notify()
方法用于唤醒线程.不管调用对象的wait()
仍是notify()
方法都先要取得该对象的对象锁.即须要在同步块中调用wait()
和notify()
.如下是MySignal.class
的修改版本,用于示例wait()
和notify()
的使用.code
public class MyWaitNotify {
private class Monitor{
}
private final Monitor monitor = new Monitor();
public void doWait(){
synchronized (monitor){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void doNotify(){
synchronized (monitor){
monitor.notify();
}
}
}
复制代码
等待线程能够调用doWait()
, 通知线程能够调用doNotify()
.当有一个线程调用notify()
,其余调用同一对象wait()
方法的线程将会被唤醒.调用一次notify()
仅会唤醒一个线程.如有多个线程调用了同一对象的wait()
方法, 能够经过调用notifyAll()
来唤醒所有线程.cdn
你能看到等待和通知线程在调用wait()
和notify()
时是在同步块中进行的, 而这是必须的.若在没有得到任意对象的对象锁前提下, 调用该对象wait()
notify()
和notifyAll()
中的任一方法都会抛出IllegalMonitorStateException
异常.htm
当等待线程在同步代码块中执行过程当中, 会不会一直取得对象锁不放, 这样其余通知线程就不能进入同步代码块来调用notify()
方法了.答案是否认的, 一旦等待线程调用wait()
方法, 会连同对象锁一块儿释放, 对notify()
的调用也是如此.这样就能让其余线程调用wait()
和notify()
方法了, 前提是他们都在同步代码块中调用.
当一个线程被唤醒时并不能立刻离开wait()
方法,还须要等待通知线程调用notify()
完毕后离开同步代码块释放锁.换句话说,被唤醒的线程须要得到对象锁才能退出wait()
方法,由于wait()
方法是在同步代码块中调用的.若是多个线程经过notifyAll()
方法被唤醒, 那么同一时刻只会有一个线程退出wait()
方法,由于其余线程退出wait()
方法必须得到对象锁.
若没有任何线程调用相同对象wait()
方法前提下, 调用notify()
和notifyAll()
, 通知信号将不会存储. 此时信号将会丢失. 若在等待线程调用wait()
方法以前就调用了notify()
方法, 那么对于等待线程来讲, 信号已经丢失了.固然这并非什么大问题, 但有些时候, 这将会致使等待线程无限期的等待下去, 由于唤醒信号已经丢失了.
为了防止这种状况发生, 咱们须要将信号量存储起来.咱们能够在上文MyWaitNotify.class
上进行改进,在该类中置放一个成员变量,用于存储信号量.
public class MyWaitNotify2 {
private class Monitor{
}
boolean wasSignalled = false;
private final Monitor monitor = new Monitor();
public void doWait(){
synchronized (monitor){
if(!wasSignalled){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
wasSignalled = false;
}
}
public void doNotify(){
synchronized (monitor){
wasSignalled = true;
monitor.notify();
}
}
}
复制代码
如今能够注意到doNotify()
方法中在调用监控对象的notify()
方法前, 先置换wasSignalled
为true
.一般在doWait()
方法中, 在调用监控对象的wait()
方法前先检查wasSignalled
的值是否为false
.若不是,则不调用wait()
方法,置换wasSignalled
为false
.这样等待线程只有在上一次调用doWait()
和这一次中间没有接收到任何信号量的前提下才会调用监控对象的wait()
方法.
出于一些未知的缘由,线程在没有调用notify()
和notifyAll()
的前提下也会被唤醒.出于未知缘由的唤醒, 咱们称之为虚假唤醒.
若是上文MyWaitNotify2.class
示例中,等待线程在doWait()
方法中发生虚假唤醒,线程将在没有接收到预期信号量的前提下执行, 这将会在你的应用中产生若干问题.
为了防止虚假唤醒的发生, 咱们须要用while()
循环检查信号量来代替if
语句.这种状况咱们称之为旋转锁.只要在旋转条件不成立时, 旋转锁才会释放.一样咱们在上文MyWaitNotify2.class
上进行改进:
public class MyWaitNotify3 {
private class Monitor{
}
boolean wasSignalled = false;
private final Monitor monitor = new Monitor();
public void doWait(){
synchronized (monitor){
while(!wasSignalled){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
wasSignalled = false;
}
}
public void doNotify(){
synchronized (monitor){
wasSignalled = true;
monitor.notify();
}
}
}
复制代码
咱们能看到, 仅仅是将原先的if
语句替换为while
. 等待线程若是在没有接收到信号量的前提下唤醒, 将会进入旋转锁中, 从新调用wait()
进入等待状态. 等待线程只有在接收到信号量后才会释放旋转锁.
当使用notifyAll()
唤醒在等待同一个信号量的多个线程且仅容许它们中一个继续运行时, while
循环是一个不错的解决方案.此时只有一个线程可以得到对象锁, 即只有一个线程可以退出wait()
方法并清除wasSignalled
标记.一旦该线程退出doWait()
方法中的同步块,其余线程能够得到对象锁退出wait()
方法, 而后进入while()
检查wasSignalled
成员变量.此时wasSignalled
标记已经被上一个唤醒的线程清除, 当前线程只能在片刻唤醒后从新进入等待状态,直到下一个信号量到来.
将上文中的MyWaitNotify3.class
示例稍做修改,使用一个空String做为监控对象.
done like this:
public class MyWaitNotify4 {
boolean wasSignalled = false;
private final String monitor = "";
public void doWait(){
synchronized (monitor){
while(!wasSignalled){
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
wasSignalled = false;
}
}
public void doNotify(){
synchronized (monitor){
wasSignalled = true;
monitor.notify();
}
}
}
复制代码
在空String或其余String常量上调用wait()
和notify()
, JVM编译器内部会将String常量转换为同一个对象.这意味着, 即便你有两个彻底不一样的MyWaitNotify
实例, 它们各自的monitor
成员变量都会指向同一个空String对象.这也意味着调用第一个MyWaitNotify
实例doWait()
方法的线程可能会被调用第二个MyWaitNotify
实例doNotify()
方法的线程唤醒.
以上状况能够用下图归纳:
记住,即便4个线程都是调用同一个共享String对象的wait()
和notify()
方法,调用doWait()
和doNotify()
所产生的信号量任然分别存储在不一样的MyWaitNotify
实例中.一个线程调用MyWaitNotify1
的doNotify()
方法可能会唤醒等待MyWaitNotify2
的线程, 但产生的信号量任然会存储在MyWaitNotify1
实例中.
这看起来好像没什么大问题,但若是第二个MyWaitNotify
实例的doNotify()
被调用, 意外唤醒了线程A和线程B, 此时线程A和B会在旋转锁中检查信号量标记后从新进入等待状态,由于第一个MyWaitNotify
实例的doNotify()
方法并无被调用.信号量标记任然是false
.这种状况相似于激活了虚假唤醒.线程A或B并无得到信号量.但旋转锁能够处理这样的状况,所以线程A或B会从新进入等待状态.
问题在于doNotify()
中调用的是notify()
,四个等待同个空String对象锁的线程仅会有一个能被唤醒,可能被唤醒的线程并非给定想要传递信号量的目标线程.若是A和B其中一个线程被唤醒,而信号量实际是给C或D的,此时A和B进入while()
检查信号量标记, 发现为false
,则从新回到等待状态.而此时C和D并无被唤醒,而信号量却到达了,所以信号量此刻丢失了.这种状况相似于前文提到的信号丢失.C和D收到了信号量却没有正确响应它.
若把doNotify()
中的notify()
换成notifyAll()
则全部的线程都会被唤醒并根据信号量做出响应.线程A和B仍然会进入等待状态,但C和D其中有一个可以正确响应信号量,由于它发现信号量被置换为true
则退出wait()
方法响应信号量并清除信号量标记.另外一个则检查到清除后的信号量从新回到等待状态.
你或许会倾向于使用notifyAll()
来代替notify()
,但实际这会带来额外的性能损耗,并非最佳选择.没有理由唤醒全部的线程来响应一个信号量.
因此千万不要调用全局对象或String常量的wait()
和notify()
方法.而是使用一个独一无二的实例对象.就像MyWaitNotify3.class
示例中,使用Monitor
实例来代替空String.
该系列博文为笔者复习基础所著译文或理解后的产物,复习原文来自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial