博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【13】线程间的信号处理
阅读量:6888 次
发布时间:2019-06-27

本文共 6144 字,大约阅读时间需要 20 分钟。

  hot3.png

  • 原文地址:
  • 作者: Jakob Jenkov

线程通信就是为了让线程可以彼此发送一些信号。另外,还可以让线程等待其他线程的某个信号。 比如,线程B可以等待线程A数据已经准备好了的信号。

通过共享对象通信

其实,线程间通信的最简单方式就是使用一个共享对象的值来作为信号。 比如,线程A可以通过一个同步块设置一个boolean的成员变量hasDataToProcess为true。 然后,线程B也在同一个同步块中读取这个成员变量的值。 这样就可以让两个线程通过一个共享变量,完成某种信号的通知。

public class MySignal{  protected boolean hasDataToProcess = false;  public synchronized boolean hasDataToProcess(){    return this.hasDataToProcess;  }  public synchronized void setHasDataToProcess(boolean hasData){    this.hasDataToProcess = hasData;    }}

要注意,线程A和线程B必须使用同一个共享对象的实例,才能完成通信。 如果,线程A和B持有着两个不同的实例引用,那彼此就无法发现对方的信号了。

忙等待(Busy Wait)

线程B如果需要等待数据准备好了才能继续处理。 也就是说,线程B在等待线程A通过hasDataToProcess()返回true的信号。 线程B如果通过一个轮询来等待信号,比如这样:

protected MySignal sharedSignal = ......while(!sharedSignal.hasDataToProcess()){  //do nothing... busy waiting}

注意,循环会一直执行,直到hasDataToProcess()返回true。 这样就称之为忙等待(busy waiting)。 线程在等待期间,一直处于一个忙碌状态。

wait(), notify() 以及 notifyAll()

忙等待的线程在等待期间对于CPU来说是一种浪费,特别是等待时间较长的情况下。 而如果,线程能够在等待时能够进行休眠或者暂停,直到等待的信号发生,那就比较聪明了。

Java有个自带的等待机制可以让线程暂停直到等待的信号发生。 在java.lang.Object类上定义了三个方法来使用这个机制:wait(),notify()以及notifyAll()方法。

当线程在任一对象上调用wait()方法,线程就会暂停,直到另一个线程在这个对象上调用notify()方法。 为了可以调用wait()notify()方法,线程需要先获得对象上的监控锁(monitor)。 也就是说wait()notify()方法,需要在同步块(synchronized block)中调用。 例如:

public class MonitorObject{}public class MyWaitNotify{  MonitorObject myMonitorObject = new MonitorObject();  public void doWait(){    synchronized(myMonitorObject){      try{        myMonitorObject.wait();      } catch(InterruptedException e){...}    }  }  public void doNotify(){    synchronized(myMonitorObject){      myMonitorObject.notify();    }  }}

需要等待的线程可以调用doWait()方法,需要发送信号(通知)的线程可以调用doNotify()方法。 当一个线程在某个对象上调用notify()方法,那在这个对象上等待的某一个线程就会被唤醒,并允许其继续执行。 而notifyAll()方法将会唤醒对象上所有等待的线程。

正如你所看到的,等待线程和通知线程对于wait()notify()都是在同步块中调用的。 这个是一个强制性的要求! 如果线程在没有获得对象的监控锁(monitor)的情况下,调用wait()notify()方法,就会得到一个IllegalMonitorStateException异常。

但是,这怎么可能呢?这样岂不是等待线程持有了对象的监控锁(monitor)然后一直等待? 而如果等待线程不释放监控锁(monitor),那岂不是通知线程就永远无法获得锁,也就永远无法发出通知了吗? 当然,不必担心这个问题,因为答案是,不会发生上述问题。 一旦线程调用了wait()方法,就会释放已经持有的监控锁(monitor)。 这就可以让其他线程去调用wait()notify()方法了,当然还是需要再次获得对象的监控锁!

等待线程被唤醒后,还不能退出wait()方法,还需要等待通知线程(调用notify()方法的线程)离开同步块,等待线程才能退出wait()方法继续执行。 也就是说,被唤醒的线程,必须在退出wait()方法之前,重新获取等待对象上的监控锁。(wait()仍然在同步块中) 如果多个线程被notifyAll()唤醒,那么只会有一个被唤醒的线程能够退出wait()方法,因为,每个线程还需要重新获得等待对象上的监控锁(monitor)才能退出wait()方法。

信号丢失(Missed Signals)

notify()notifyAll()并不会在没有等待线程的情况下,保持方法调用的状态。 也即是,通知信号是一次性的,不会保存信号状态,而是直接丢弃。 因此,如果线程调用notify()方法先于另一个线程调用wait()方法,那信号对于等待线程就是丢失的。 这是不是一个问题,要看具体的情况。在某些情况下,这会导致等待线程永远等待下去,永远不会被唤醒。

如果需要避免信号丢失的情况,那就需要将信号存储类中。下面这个例子就展示了通过成员变量来存放信号:

public class MyWaitNotify2{  MonitorObject myMonitorObject = new MonitorObject();  boolean wasSignalled = false;  public void doWait(){    synchronized(myMonitorObject){      if(!wasSignalled){        try{          myMonitorObject.wait();         } catch(InterruptedException e){...}      }      //clear signal and continue running.      wasSignalled = false;    }  }  public void doNotify(){    synchronized(myMonitorObject){      wasSignalled = true;      myMonitorObject.notify();    }  }}

doNotify()方法在调用notify()之前,会将wasSignalled变量更新为true。 同样,doWait()方法,会在wait()之前,检查wasSignalled。 这样,如果信号发生于wait()之前,那doNotify()就不会在错过信号了。

伪唤醒(Spurious Wakeups)

由于某些原因,会导致即使没有调用notify()notifyAll()方法,线程也可能被唤醒。 这就是传说中的伪唤醒(Spurious Wakeups),毫无理由的唤醒。

使用了多线程API(POSIX 线程/Windows API)中的条件变量而带来的并发症。JVM实现中使用了条件变量,所以,Java中也伴有伪唤醒的问题。

如果MyWaitNofify2类中的doWait()方法发生伪唤醒,就会导致线程在没有收到通知信号的情况下继续执行后续代码!这可能会给你的应用带来一些问题。

为了防止伪唤醒带来的问题,可以使用一个成员变量来记录状态,并使用循环进行检查(或者说,再次确认)。 这样的循环,也可以称之为:自旋锁。被唤醒的线程通过自旋进行等待,直到自旋锁条件达成(while的条件)。示例如下:

public class MyWaitNotify3{  MonitorObject myMonitorObject = new MonitorObject();  boolean wasSignalled = false;  public void doWait(){    synchronized(myMonitorObject){      while(!wasSignalled){        try{          myMonitorObject.wait();         } catch(InterruptedException e){...}      }      //clear signal and continue running.      wasSignalled = false;    }  }  public void doNotify(){    synchronized(myMonitorObject){      wasSignalled = true;      myMonitorObject.notify();    }  }}

使用一个while循环来替代原有的if语句。如果等待线程不是因为收到通知而被唤醒的,那wasSignalled变量应该还是false,所以循环会再次进入等待。 这样就避免了伪唤醒带来的非预期中的唤醒。

在同一个信号上等待的多个线程

如果有多个线程在等待,当这些线程被notifyAll()方法唤醒时,但是只能有一个线程能够继续执行。这种情况下,while循环也是一个很好的解决方案。

同一个时间点,只能有一个线程能够获得对象的监控锁(monitor),这就意味着,只会有一个线程能够退出wait()方法并清理wasSignalled标识。 而一旦这个线程退出doWait()中的同步块,其他线程就可以退出wait(),并且再次检查wasSignalled变量。 但wasSignalled已经被第一个唤醒的线程清理掉了,所以,这个线程会随着循环再次进入等待,等待下一个信号到达。

不要在常量字符串或者全局对象上等待

下面这个例子,使用一个常量字符串""(不是new出来的String,全局唯一)作为监控对象:

public class MyWaitNotify{  String myMonitorObject = "";  boolean wasSignalled = false;  public void doWait(){    synchronized(myMonitorObject){      while(!wasSignalled){        try{          myMonitorObject.wait();         } catch(InterruptedException e){...}      }      //clear signal and continue running.      wasSignalled = false;    }  }  public void doNotify(){    synchronized(myMonitorObject){      wasSignalled = true;      myMonitorObject.notify();    }  }}

在空字符串(或者其他什么常量字符串都一样)上调用wait(),notify()方法,JVM/编译器就会转换成全局的一个对象(字符串,常量池)。 这就意味着,虽然有两个MyWaitNotify实例对象,但是它们都引用着同一个字符串实例。 这也就是说,线程在其中一个MyWaitNotify上调用doNotify(),就会导致另一个MyWaitNotify上等待的线程被唤醒。

情况如下图所示:

image

这个例子中,有4个线程在同一个共享字符串实例上调用wait()notify()方法,doWait()doNotify()方法会把信号分别存放在两个MyWaitNotify实例中。 这样的情况,会导致MyWaitNotify 1上的doNotify()方法会唤醒MyWaitNotify 2上等待的线程,而且,信号只会存放于MyWaitNotify 1中。

看上去貌似不是个大问题,毕竟,如果第二个MyWaitNotify 2实例上调用doNotify()会导致线程A和B被错误的唤醒。而A,B还会再次检查信号状态(while循环),由于不是MyWaitNotify 1的doNotify()发出的信号,所以A,B还会再次进入等待。 这就有点类型伪唤醒。 线程A/B不是被正确信号唤醒的。 但是代码可以处理这个情况,让线程A/B再次进入等待。

**(但是不得不否认,带来了额外的处理,以及不必要的上下文切换)**

而真正的问题是,由于doNotify()只是调用了notify()而不是notifyAll(),这样,就只会有一个线程被唤醒,而不是在同一个对象上等待的4个线程。 如果,被唤醒的是A或B,而型号却是来自C或D。这种情况下,A/B会再次检查状态,重新进入等待。但关键是,C/D就会错过这个信号。 这就类似上述的**信号丢失(Missed Signals)**问题了。C/D发送了信号,但是自己却不能接收到。

如果把doNotify()方法,改用notifyAll()来发送信号,那所有的等待线程就会被唤醒,并检查信号状态。 线程A/B还是再次进入等待,而C或者D就会响应这个信号,他俩中的一个(抢到monitor的)就可以离开doWait()方法,另一个也会再次进入等待。

这样,你就可能觉得总是使用notifyAll()就会比notify()更好。但其实不是,notifyAll()同样会引发更多不必要的唤醒,更多的上下文切换。

所以记住,不要使用全局对象(如:常量字符串)来实现wait()/notify()机制。 为每一组需要此机制的任务,分别创建一个对象用于信号会更好!

补充资料

转载于:https://my.oschina.net/roccn/blog/1525677

你可能感兴趣的文章
通过一张图说说测试职业如何发展(二)
查看>>
70道经典Android口试题加答案--首要常识点几乎都涉及到了
查看>>
ls命令
查看>>
Oracle 之网络配置
查看>>
Centos提示-bash:make: command not found的解决办
查看>>
puppet自动化运维之package资源
查看>>
使用Node.js搭建微信支付后台(二)
查看>>
序列化与反序列化
查看>>
debian下安装openldap
查看>>
基于域的无线安全认证方案
查看>>
百度开源高性能高可用分布式文件系统BFS
查看>>
Android平板开发永久实现全屏的方法
查看>>
windows远程连接失败的原因
查看>>
我的友情链接
查看>>
JSCH会大量使用服务器内存吗?会
查看>>
2017年围绕自动驾驶会出现新一轮的淘金热,这是真的吗?
查看>>
Centos下邮件服务器(postfix)的配置(一)
查看>>
深夜,想到今天学的linux内容,太值了
查看>>
Thread类常用方法
查看>>
/etc/resolv.conf中内容被清空的解决办法
查看>>