线程通信

原文地址:http://tutorials.jenkov.com/java-concurrency/thread-signaling.html

线程通信

  • 信号用于对象之间共享
  • 忙碌等待
  • wait(), notify() and notifyAll()
  • 信号丢失
  • 伪唤醒
  • 多个线程等待同个信号
  • 不要在String常量或者全局对象中调用wait()

线程通信的目地是允许线程之间相互传递信号,另外,线程通信还允许一个线程等待另一个线程的信号,线程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都要有指向共享MySignal实例的引用。线程A和线程B指向不同的MySignal实例,它们将不会检测到来自各自的信号。线程A和线程B要处理的数据可以位于与MySignal实例分离的共享缓冲区中。

忙碌等待

处理数据的线程B在等待数据,才能进行处理工作。换句话说,线程B在等待来自线程A因为调用hasDataToProcess()方法而返回true的信号,下面是线程B为了等到该信号,一直在跑的循环:

protected MySignal sharedSignal = ...

...

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

注意线程B会一直执行循环直到别的线程调用hasDataToProcess()返回true(hasDataToProcess 变为true),这种行为成为忙碌等待,线程因为等待而变得忙碌。

wait(), notify() and notifyAll()

忙碌等待在计算机运行正在等待的线程不是一个高效利用CPU的方法,除非平均等待时间非常短。否则,让等待的线程在接收到等待的信号之前,保持休眠或者不活跃状态会更加有效。

Java有一个内置机制可以让线程在等待信号的时候变为不活跃状态。 为了实现这个功能,java.lang.Object定义了三个方法:wait()、notify()和notifyAll()

一个线程在任意对象内调用 wait()方法而变为不活跃状态,直到另外一个线程在该对象内调用notify()唤醒不活跃的线程。为了调用wait() 或 notify(),调用者的线程必须先获得该对象的锁。也就是说,调用者的线程必须在一个同步代码块中调用wait() 或 notify(),下面是对MySignal 例子的修改版本,叫做MyWaitNotify ,使用了wait() 和 notify()。

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异常。

但是,这怎么可能(wait() 和 notify()调用都需要在获得对象锁的情况下进行,不会相互抢锁吗)?只要在同步代码块中执行,等待的线程难道不会一直持有监听对象的锁吗?等待的线程不会阻止通知线程进入同步代码块调用notify()方法吗?答案是不会的。一旦线程调用wait()方法,就会释放在监听对象上持有的锁,这就允许其他线程也调用wait() 和 notify(),因为这些方法必须在同步代码块中调用。

一旦线程被唤醒,就不能退出wait()调用,一直到线程调用notify()离开同步代码块。也就是说,被唤醒的线程必须在退出wait()调用之前重新获得监听对象的锁,因为wait()的调用时在同步代码块中的。如果通过 notifyAll()唤醒多个线程,只有一个线程可以退出wait()方法,因为每个线程在退出wait()之前都要获得监听对象的锁。

信号丢失

notify() 和 notifyAll() 不会保存方法调用,以防没有线程在等待的时候就调用它们,通知的信号可能丢失。所以,如果一个线程调用notify()在线程发出信号之前调用wait(),信号将会被等待的线程丢失(就是notify()刚调用,还没来得及通知给将要调用的wait()的线程,wait()就被调用了)。这有可能会造成问题,有可能不会,但是,在某些情况下,会造成等待线程一直在等待,永远不会被唤醒,因为唤醒的信号丢失了。

为了避免信号丢失,信号应该存储在信号类中。在MyWaitNotify的示例中,通知信号应该被存储在MyWaitNotify 的一个成员变量中。下面是MyWaitNotify 的修改版本:


  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()调用和它之间没有接收到信号,它只调用wait()。

伪唤醒

出于无法解释的原因,就算wait()和notifyAll()没被调用,线程仍然可能被唤醒。这就是所谓的伪唤醒。没有任何理由的唤醒。

如果MyWaitNotify2类中的doWait()方法中出现了伪唤醒,那么等待的线程可能会继续处理而不会收到正确的信号。这可能会在你的程序中造成严重的问题。

为了保证杜绝伪wakeups,信号成员变量在while循环中检查,替代之前的if语句。这种while循环也称为自旋锁。直到旋转锁(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()调用,并清除wassignated的值(设置为false)。一旦该线程在doWait()方法中退出同步代码块,其他线程就可以退出wait()调用,并在while循环中检查wassignated成员变量的值。然而,这个标志只会被第一个唤醒的线程清除,所以剩下的被唤醒的线程继续回到等待状态,直到下一个信号到达。

不要在String常量或者全局对象中调用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实例,它们都引用相同的空字符串实例。这也意味着在第一个MyWaitNotify实例中调用doWait()的线程将冒险被第二个MyWaitNotify实例中调用doNotify()唤醒。

如下图所示:

这里写图片描述

记住,即使4个线程可以在相同的共享字符串中调用wait()和notify(),可是从doWait() 和 doNotify()调用的信号是分别存储在两个MyWaitNotify实例中的。 A在MyWaitNotify1中调用doNotify() 可能唤醒在MyWaitNotify2中等待的线程,但是信号只会存储在MyWaitNotify1中。

刚开始这看起来似乎不是个大问题。毕竟,如果doNotify()能在第二个MyWaitNotify实例中被调用,那么最终会发生的就是线程A和线程B被错误的唤醒。这个被唤醒的线程(A或B)将会在while循环中检查信号,然后继续回到等待状态,因为第一个MyWaitNotify实例没有调用doNotify(),所以他们会一直等待。这种情况等价于故意造成伪唤醒。线程A和线程A没有信号就被唤醒。但是代码会处理,让它们回去继续等待。

问题在于,doNotify()中只调用了notify()而不是notifyAll(),只有一个线程被唤醒,即使有4个线程在相同的字符串常量(空字符串)中等待。如果线程A和线程B中的一个整的接收来自线程C或线程D的信号而被唤醒,被唤醒的线程(A或B)会检查信号,看看是否接受到信号(实际上接收不到,因为信号存储在MyWaitNotify2中),然后继续回到等待状态。C和D通过检查接收信号同样也不会被唤醒,所以信号丢失了。这种情况等价于过早的丢失信号问题。C和D发送了信号却没有接收到。

如果doNotify()方法是调用notifyAll()而不是notify(),所有的线程都会被唤醒并轮流检查信号。A和B会回到等待状态,但是C和D中的一个会注意到信号并离开doWait() 。C和D的另一个会返回等待状态,因为线程发现信号后会在doWait()外边的路上清除信号。

你可能想经常调用notifyAll()而不是notify(),但这不是一个明智的想法。从来没有理由在只有一个线程可以响应信号时去唤醒所有线程。

所以:在wait() / notify() 机制中,不要使用全局变量,字符串常量等等。使用一个唯一的构造函数构造的对象。例如,每个MyWaitNotify3(上面的示例)都有自己的MonitorObject实例,而不是使用空字符串wait()/ notify()来调用。

关于线程通信,这里参考Thinking In Java中的一个例子:

package com.wen.java.concurrent.myrelationship;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//食物类
class Meal{
    //事物的单号
    private final int orderNum;
    public Meal(int orderNum) {
        this.orderNum = orderNum;
    }
    public String toString(){
        return "Meal orderNum: " + orderNum;
    }
}

//食客
class WaitPerson implements Runnable {
    private Restaurant restaurant;
    public WaitPerson(Restaurant restaurant){
        this.restaurant = restaurant;
    }
    public void run() {
        try{
            while(!Thread.interrupted()) {
                //食物被消费
                synchronized(this) {
                    while(restaurant.meal == null) {
                        this.wait();
                    }
                }
                System.out.println("waitPerson got" + restaurant.meal);
                //食物被消费了,那么,厨师就得开始工作了
                synchronized(restaurant.chef) {
                    restaurant.meal = null;
                    restaurant.chef.notifyAll();
                }
            }
        }catch(InterruptedException e) {
            System.out.println("WaitPerson interrupted");
        }   
    }
}

//厨师
class Chef implements Runnable {
    private Restaurant restaurant;
    private int count = 0;
    public Chef(Restaurant restaurant){
        this.restaurant = restaurant;
    }
    public void run() {
        try{
            while(!Thread.interrupted()) {
                synchronized(this) {
                    while(restaurant.meal != null) {
                        this.wait();
                    }
                }
                //厨师生产了食物
                System.out.println("Order up");
                if(count++ > 10) {
                    //打烊了
                    System.out.println("Out of food! close");
                    restaurant.exec.shutdownNow();
                }
                //生产出一个食物
                synchronized(restaurant.waitPerson) {
                    restaurant.meal = new Meal(count);
                    //唤醒客人吃饭了
                    restaurant.waitPerson.notifyAll();
                }
            }
        }catch(InterruptedException e) {
            System.out.println("Chef interrupted");
        }   
    }
}


public class Restaurant {
    Meal meal;
    //客人和厨师都在餐厅里面
    WaitPerson waitPerson = new WaitPerson(this);
    Chef chef = new Chef(this);
    ExecutorService exec = Executors.newCachedThreadPool();
    public Restaurant(){
        exec.execute(chef);
        exec.execute(waitPerson);
    }
    public static void main(String[] args) {
        new Restaurant();
    }
}
/*~output
Order up
waitPerson gotMeal orderNum: 1
Order up
waitPerson gotMeal orderNum: 2
Order up
waitPerson gotMeal orderNum: 3
Order up
waitPerson gotMeal orderNum: 4
Order up
waitPerson gotMeal orderNum: 5
Order up
waitPerson gotMeal orderNum: 6
Order up
waitPerson gotMeal orderNum: 7
Order up
waitPerson gotMeal orderNum: 8
Order up
waitPerson gotMeal orderNum: 9
Order up
waitPerson gotMeal orderNum: 10
Order up
waitPerson gotMeal orderNum: 11
Order up
Out of food! close
WaitPerson interrupted
*/
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值