15.线程信号(Signaling)

线程信号的目的是使得线程彼此之间可以互相发送信号。另外,线程信号可以使得线程去等待来自其他线程的信号。例如,一个线程B可能等待来自一个线程A的信号,说明数据准备处理了。

经由共享变量的信号

对于线程互相发送信号的一个简单方式就是在一些共享的对象变量中设置一个信号值。线程A可能设置这个布尔的成员变量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为了发送信号去工作必须有一个对于这个共享的MySignal示例的引用。如果线程A和线程B有一个不同的示例的引用,他们不会探测到彼此的信号。将要被处理的数据可能会位于分离的MySignal示例的内存缓存区。

忙于等待

线程B将要去处理它等待的变得可以用的数据。换句话说,它正在等待来自线程A的一个信号,返回true的一个信号。这里线程B运行的是一个回路,当等待这个信号量的时候:

protected MySignal sharedSignal = ...

...

while(!sharedSignal.hasDataToProcess()){
  //do nothing... busy waiting
}
注意这个循环一直会保持执行,直到返回true的时候。这个被称之为忙于等待。这个线程当等待的时候是繁忙的。

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

忙于等待对于运行这个等待线程的计算机CPU利用率不是很有效率的,除了这个平均等待时间非常短。另外,如果这个等待的线程可以某些时候睡眠或者变成未激活的直到它接收到这个信号量,这个将会是更加智能的。

Java有一个內建的等待机制,当等待信号的时候使得线程变得未激活的。这个java.lang.Object类定义了三个方法,wait(),notify()以及notifyAll(),去处理这个。

一个线程在任何对象上调用wait方法将会变得未激活的,直到另外一个线程在那个对象上调用notify方法。为了调用wait方法或者notify方法,这个调用的线程必须首先获取在那个对象上的锁。换句话说,这个调用的线程必须在一个同步代码块的内部调用wait或者notify方法。这里有一个MySignal使用wait方法和notify方法调用MyWaitNotify的修改版本:

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方法。这个是强制的。一个线程不能调用wait方法,notify方法以及notifyAll方法,如果没有持有调用这个方法的对象的锁。如果这样做了,一个IllegalMonitorStateException异常将会抛出。

但是,这个怎么可能呢?等待的线程不会持有这个监控的对象的锁,只要它正在一个同步代码块内部执行?这个等待的线程将不会堵塞这个来自于进入到doNotify方法的同步代码块的通知线程吗?答案是不会的。一旦一个线程调用wait方法,它将会释放它持有的监控对象的锁。这个也允许其他的线程去调用wait方法和notify方法,因为这些方法必须在一个同步代码块的内部被调用。

一旦一个线程被唤醒了,它直到调用notify方法的线程已经离开同步代码块才会离开wait方法。换句话说,这个唤醒的线程在它可以离开这个wait方法调用之前必须重新获取监控对象的锁,因为这个等待调用在同步代码块内部是嵌套的。如果使用notifyAll方法使得多个线程被唤醒,那么一次只能有一个被唤醒的线程退出wait方法,因为每一个线程必须轮流的获取监控对象的锁在退出之前。

丢失信号

这个notify和notifyAll方法当他们被调用的时候一旦没有线程正在等待了不会保存调用他们的方法。这个通知的信号然后就丢失了。因此,如果一个线程在它去发信号之前调用notify方法已经调用wait方法,那这个信号就会被正在等待的线程丢失。这个可能是一个问题,也可能不是,但是在一些场景中这个可能会导致这个等待的线程永远等待下去,不会唤醒,因为去唤醒的信号丢失了。

为了避免丢失信号他们应该被存储在信号类的内部。在MyWaitNotify实例中,这个通知信号应该被存储到MyWaitNotify示例内部的成员变量中。这里有一个修改的版本如下:

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方法现在设置这个wasSignalled变量为true在调用notify之前。也要注意这个doWait方法现在在调用wait方法之前需要检查下这个Boolean变量。事实上,在以前的doWait方法调用和这个之间如果没有信号被接收它只是调用wait方法。

假的唤醒

因为一些莫名其妙的原因,对于线程甚至如果没有调用notify方法和notifyAll方法也可能会被唤醒。这是以假醒而出名。没有任何原因。

如果一个假的唤醒发生在MyWaitNotify2类中的doWait方法,那么这个等待的线程在没有接收到正确的信号就会去继续执行。这个可能在你的应用中会引起严重的问题。

为了防止假醒,这个信号成员变量在while循环中被检查代替if条件语句。这样的一个while循环也被称之为自旋锁。唤醒的线程自旋直到在自旋锁的条件变成false。这里有一个修改版如下:

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();
    }
  }
}

注意这个wait方法调用现在是嵌套在一个while循环中代替if条件判断。如果等待的这个线程在没有接收到一个信号的时候醒了,这个wasSignalled成员将会仍然是false,并且这个while循环将会再一次执行,引起这个唤醒的线程返回去等待。

等待相同信号的多个线程

如果你有多个线程正在等待的话,这个while循环也是一个好的解决方案,使用notifyAll一起会被唤醒,但是他们中应该只有一个允许去继续。只能一次有一个线程能够获得这个监控对象的锁,意味着只能一个线程可以离开wait调用,以及清除这个wasSignalled标识。一旦这个线程退出doWait方法的同步块,其他的线程可以退出wait调用以及检查这个while循环内部的wasSignalled变量。然而,这个标识会被第一个唤醒的线程清理了,以至于唤醒的线程剩下的将会返回去等待,直到下一个信号到达。

不要在字符串常量或者全局对象上调用wait方法

这个文本的一个更早的版本有一个MyWaitNotify实例类的一个版本,它是使用一个常量字符串作为监控对象。这里有一个例子:

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实例,他们都引用了相同的空的字符串实例。这也就意味着在第一个实例中调用doWait方法将会有被第二个实例的doNotify方法唤醒的风险。

这个场景可以见下面的简单的图示:



记住,甚至如果这四个线程在相同的共享字符串实例上调用wait方法和notify方法,来自doWait方法和doNotify方法调用的信号会单独的存储在两个MyWaitNotify实例中。在实例1上的doNotify方法调用可能会唤醒在实例2上等待的线程,但是这个信号指示存储在实例1上。

首先,这个可能看起来不是一个大的问题。毕竟,如果doNotify方法在第二个实例上被调用,所有的这样真的发生就是线程A和线程B会错误的唤醒。这个唤醒的线程(A或者B)将会在while循环中检查他们的信号,并且因为doNotify方法在第一个实例中没有被调用会返回去等待,在这种情况下他们都会等待。这种场景相当于激起了一个假的唤醒。线程A或者线程B没有得到信号。但是代码可以处理这个,以至于这个线程会返回去等待。

这个问题是,因为doNotify调用只是调用notify方法而不是notifyAll方法,甚至如果4个线程正在等待相同的字符串实例(这个空的字符串),那么只能有一个线程会被唤醒的。以至于,如果这个线程A或者B中的一个线程被唤醒当真正的信号是为了C或者D的时候,这个唤醒的线程(A或者B)将会检查他们的信号,看到没有信号接收到,然后返回去等待。C或者D都不会检查他们真正接收的那个信号,以至于这个信号丢失了。这个场景等同于前面描述的丢失信号的问题。C和D发送一个信号但是错误的响应它。

如果这个doNotify方法已经调用了notifyAll代替notify方法,所有正在等待的线程将会被唤醒并且轮流检查这个信号。线程A和B将会返回去等待,但是C或者D中的一个将会接收到这个信号并且离开这个doWait方法的调用。剩下的将会返回去等待,因为发现这个信号的线程如果离开了这个doWait方法将会清除它。

你可能会尝试着总是去调用notifyAll代替notify方法,但是这是一个不好的性能选择。这里没有理由去唤醒所有正在等待的线程当他们只有一个可以响应这个信号的时候。

所以:不要使用全局对象,字符串常量等等。对于wait方法/notify方法的机制。使用一个对象对于使用它的构造函数是唯一的。例如,每一个MyWaitNotify3(来自前面部分的例子)实例都有他自己的MonitorObject实例而不是使用这个空的字符串为了wait()/notify()调用。


翻译地址:http://tutorials.jenkov.com/java-concurrency/thread-signaling.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值