JAVA线程间通讯简介

线程通讯的目标是使线程间可以互相发送信号。另外一方面,线程通讯使线程可以等待其余线程的信号。html

通讯

例如,线程B能够等待线程A的一个信号,这个信号会通知线程B数据已经准备好了。本文将讲解如下几个JAVA线程间通讯的主题:java

一、经过共享对象通讯segmentfault

二、忙等待缓存

三、wait(),notify()和notifyAll()数据结构

四、丢失的信号多线程

五、假唤醒并发

六、多线程等待相同信号性能

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

一、经过共享对象通讯

线程间发送信号的一个简单方式是在共享对象的变量里设置信号值。线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步块里读取hasDataToProcess这个成员变量。这个简单的例子使用了一个持有信号的对象,并提供了set和check方法:spa

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共享实例的引用,以便进行通讯。若是它们持有的引用指向不一样的MySingal实例,那么彼此将不能检测到对方的信号。须要处理的数据能够存放在一个共享缓存区里,它和MySignal实例是分开存放的。

二、忙等待(Busy Wait)

准备处理数据的线程B正在等待数据变为可用。换句话说,它在等待线程A的一个信号,这个信号使hasDataToProcess()返回true。线程B运行在一个循环里,以等待这个信号:

protected MySignal sharedSignal = ...

...

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

三、wait(),notify()和notifyAll()

忙等待没有对运行等待线程的CPU进行有效的利用,除非平均等待时间很是短。不然,让等待线程进入睡眠或者非运行状态更为明智,直到它接收到它等待的信号。

Java有一个内建的等待机制来容许线程在等待信号的时候变为非运行状态。java.lang.Object 类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。

一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另外一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先得到那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。如下是MySingal的修改版本——使用了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异常。

(校注:JVM是这么实现的,当你调用wait时候它首先要检查下当前线程是不是锁的拥有者,不是则抛出IllegalMonitorStateExcept,参考JVM源码的 1422行。)

可是,这怎么可能?等待线程在同步块里面执行的时候,不是一直持有监视器对象(myMonitor对象)的锁吗?等待线程不能阻塞唤醒线程进入doNotify()的同步块吗?答案是:的确不能。一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将容许其余线程也能够调用wait()或者notify()。

一旦一个线程被唤醒,不能马上就退出wait()的方法调用,直到调用notify()的线程退出了它本身的同步块。换句话说:被唤醒的线程必须从新得到监视器对象的锁,才能够退出wait()的方法调用,由于wait方法调用运行在同步块里面。若是多个线程被notifyAll()唤醒,那么在同一时刻将只有一个线程能够退出wait()方法,由于每一个线程在退出wait()前必须得到监视器对象的锁。

四、丢失的信号(Missed Signals)

notify()和notifyAll()方法不会保存调用它们的方法,由于当这两个方法被调用时,有可能没有线程处于等待状态。通知信号事后便丢弃了。所以,若是一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。这多是也可能不是个问题。不过,在某些状况下,这可能使等待线程永远在等待,再也不醒来,由于线程错过了唤醒信号。
为了不丢失信号,必须把它们保存在信号类里。在MyWaitNotify的例子中,通知信号应被存储在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()方法在调用notify()前把wasSignalled变量设为true。同时,留意doWait()方法在调用wait()前会检查wasSignalled变量。事实上,若是没有信号在前一次doWait()调用和此次doWait()调用之间的时间段里被接收到,它将只调用wait()。

(校注:为了不信号丢失, 用一个变量来保存是否被通知过。在notify前,设置本身已经被通知过。在wait后,设置本身没有被通知过,须要等待通知。)

五、假唤醒

因为莫名其妙的缘由,线程有可能在没有调用过notify()和notifyAll()的状况下醒来。这就是所谓的假唤醒(spurious wakeups)。无故端地醒过来了。

若是在MyWaitNotify2的doWait()方法里发生了假唤醒,等待线程即便没有收到正确的信号,也可以执行后续的操做。这可能致使你的应用程序出现严重问题。

为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫作自旋锁(校注:这种作法要慎重,目前的JVM实现自旋会消耗CPU,若是长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大)。被唤醒的线程会自旋直到自旋锁(while循环)里的条件变为false。如下MyWaitNotify2的修改版本展现了这点:

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循环会再执行一次,促使醒来的线程回到等待状态。

六、多个线程等待相同信号

若是你有多个线程在等待,被notifyAll()唤醒,但只有一个被容许继续执行,使用while循环也是个好方法。每次只有一个线程能够得到监视器对象锁,意味着只有一个线程能够退出wait()调用并清除wasSignalled标志(设为false)。一旦这个线程退出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/编译器内部会把常量字符串转换成同一个对象。这意味着,即便你有2个不一样的MyWaitNotify实例,它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个MyWaitNotify实例上调用doWait()的线程会被在第二个MyWaitNotify实例上调用doNotify()的线程唤醒。这种状况能够画成如下这张图:

起初这可能不像个大问题。毕竟,若是doNotify()在第二个MyWaitNotify实例上被调用,真正发生的事不外乎线程A和B被错误的唤醒了 。这个被唤醒的线程(A或者B)将在while循环里检查信号值,而后回到等待状态,由于doNotify()并无在第一个MyWaitNotify实例上调用,而这个正是它要等待的实例。这种状况至关于引起了一次假唤醒。线程A或者B在信号值没有更新的状况下唤醒。可是代码处理了这种状况,因此线程回到了等待状态。记住,即便4个线程在相同的共享字符串实例上调用wait()和notify(),doWait()和doNotify()里的信号还会被2个MyWaitNotify实例分别保存。在MyWaitNotify1上的一次doNotify()调用可能唤醒MyWaitNotify2的线程,可是信号值只会保存在MyWaitNotify1里。

问题在于,因为doNotify()仅调用了notify()而不是notifyAll(),即便有4个线程在相同的字符串(空字符串)实例上等待,只能有一个线程被唤醒。因此,若是线程A或B被发给C或D的信号唤醒,它会检查本身的信号值,看看有没有信号被接收到,而后回到等待状态。而C和D都没被唤醒来检查它们实际上接收到的信号值,这样信号便丢失了。这种状况至关于前面所说的丢失信号的问题。C和D被发送过信号,只是都不能对信号做出回应。

若是doNotify()方法调用notifyAll(),而非notify(),全部等待线程都会被唤醒并依次检查信号值。线程A和B将回到等待状态,可是C或D只有一个线程注意到信号,并退出doWait()方法调用。C或D中的另外一个将回到等待状态,由于得到信号的线程在退出doWait()的过程当中清除了信号值(置为false)。

看过上面这段后,你可能会设法使用notifyAll()来代替notify(),可是这在性能上是个坏主意。在只有一个线程能对信号进行响应的状况下,没有理由每次都去唤醒全部线程。

因此:在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应惟一的对象。例如,每个MyWaitNotify3的实例(前一节的例子)拥有一个属于本身的监视器对象,而不是在空字符串上调用wait()/notify()。

校注:

<

p>管程 (英语:Monitors,也称为监视器) 是对多个工做线程实现互斥访问共享资源的对象或模块。这些共享资源通常是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行它的某个子程序。与那些经过修改数据结构实现互斥访问的并发程序设计相比,管程很大程度上简化了程序设计。

<

p>


原文 Thread Signaling
译者:杜建雄 校对:方腾飞
via ifeve

相关文章
相关标签/搜索