专栏首页desperate633Java线程通信(Thread Signaling)利用共享对象实现通信忙等(busy waiting)wait(), notify() and notifyAll()信号丢失(Missed Sign

Java线程通信(Thread Signaling)利用共享对象实现通信忙等(busy waiting)wait(), notify() and notifyAll()信号丢失(Missed Sign

  • 利用共享对象实现通信
  • 忙等(busy waiting)
  • wait(), notify() and notifyAll()
  • 信号丢失(Missed Signals)
  • 虚假唤醒(Spurious Wakeups)
  • 多个线程等待相同的信号
  • 不要对String对象或者全局对象调用wait方法

线程通信的目的就是让线程间具有互相发送信号通信的能力。 而且,线程通信可以实现,一个线程可以等待来自其他线程的信号。举个例子,一个线程B可能正在等待来自线程A的信号,这个信号告诉线程B数据已经处理好了。

利用共享对象实现通信

一个实现线程通信的简单的方式就是通过在某些共享的对象变量中设置一个信号值。举个例子,线程A在一个synchronize的语句块中设置一个boolean的成员变量hasDataToProcess为true,线程B在一个synchronize语句块中读取hasDataToProcess,如果为true就执行代码,否则就等待。这样就实现了线程A对线程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类的对象实例的引用。如果线程拥有的是不同的实例,那么他们就无法获取到对方的信号。

忙等(busy waiting)

线程B执行的条件是,等待线程A发出通知,也就是等到线程A将hasDataToProcess()设置为true,所以线程b一直在等待信号,在一个循环的检测条件中。这时候线程B就处于一个忙等的状态。,因为线程b在等待的过程中是忙碌的,因为线程B在不断的循环检测条件是否成功。

protected MySignal sharedSignal = ...

...

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

wait(), notify() and notifyAll()

忙等对于cpu的利用不是一个有效率的选择,除非忙等的时间是非常短的。不然,与其让线程处于忙等的状态,不如直接让线程直接sleep,直到它收到信号再重新激活它。

Java有一个内置的方法,可以让线程在等待信号的变为inactive状态。所有类的超类 java.lang.Object 定义了三个方法, wait(), notify(), and notifyAll()

一个线程可以对任何一个对象调用wait方法,这样这个线程就会变成wait状态,inactive,等待其他线程在同一个对象上调用notify方法,来唤醒这个线程。值得注意的是,在调用wait和notify方法之前,必须要先获得这个对象的锁。换句话说,线程必须在synchronize的语句块中调用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方法,notify线程可以调用donotify方法。当一个线程在一个对象上调用notify方法的时候,这个对象的等待线程队列中的一个线程会被唤醒,获得执行的权利。notifyAll方法则是会将给定对象的等待队列中的所有线程都唤醒。

我们可以看到我们调用wait或者notify方法的时候,都是在synchronize语句块中调用的。这是一个必要条件。一个线程如果没有取得相关对象的锁则无法调用wait和notify方法,会抛出IllegalMonitorStateException异常。

一旦一个线程调用wait方法,他就会释放锁,这就允许其他线程去继续调用wait方法或者notify方法,所以这些方法都必须出现在synchronize语句块中。

一个线程如果被唤醒了,不会立即离开wait方法,因为还没获得锁,要等到那个调用notify的线程离开他的synchronize的语句块,也就是等待他释放锁,才可以获得锁,离开wait。换句话说,换句话,线程要离开wait方法,必须重新获得锁相应对象的锁。如果多个线程被notifyall方法唤醒,那么在某一个时刻,只有一个被唤醒的线程可以离开wait方法,因为每个都必须重新获得锁才可以离开wait方法。

信号丢失(Missed Signals)

如果在调用notify或者notifyAll的时候,线程等待队列中,没有线程在等待,那么这个唤醒的信号并不会被保存。而是会丢失。所以,如果一个线程在另一个线程调用wait方法等待之前,就调用了notify方法,那么这个notify的信号就被丢失了,这就可能导致那个等待的线程将一直不会被唤醒,因为notify的唤醒信号丢失了。

To avoid losing signals they should be stored inside the signal class. In the MyWaitNotify example the notify signal should be stored in a member variable inside the MyWaitNotify instance. Here is a modified version of MyWaitNotify that does this: 为了避免信号的丢失,我们可以想办法将信号存起来,利用一个变量。如下面这个例子:

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

我们可以看到,上面的方法在调用notidy之前先将wasSignalled设置为true。dowait方法会先检查wasSignalled变量,如果为true,就直接跳过wait方法,因为已经有notify信号发出了。如果为false,则说明还没有信号发出,就进入wait方法,进行等待。所以,我们利用一个boolean变量就可以解决通知过早的问题。

虚假唤醒(Spurious Wakeups)

有时候因为某些原因,线程可能会在没有调用notify或者notifyAll的情况下被唤醒,这也叫做虚假唤醒(Spurious Wakeups)。如果一个线程被虚假唤醒就会产生很多意想不到的问题,所以必须重视这个问题。

我们使用一个自旋锁机制,也就是用while循环替代if循环,循环检查这样就可以避免虚假唤醒的情况。

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循环里,如果一个线程被唤醒,但是没有获得信号,那么wasSignalled 仍是false,while循环会进行多次判断,重新将线程变为wait。

我们更好的理解,我们举一个具体的例子: 假设有两个类负责加减:

package Thread;

public class Add {
    private String lock;
    
    public Add(String lock) {
        super();
        this.lock = lock;
    }
    
    public void add() {
        synchronized (lock) {
            ValueObject.list.add("anything");
            lock.notifyAll();
        }
    }
}
package Thread;

public class Subtract {
    private String lock;
    public Subtract(String lock) {
        super();
        this.lock = lock;
    }
    
    public void subtract() {
        try {
            synchronized (lock) {
                if(ValueObject.list.size() == 0) {
                    System.out.println("Wait begin ThreadName:" + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("Wait end ThreadName:" + Thread.currentThread().getName());
                }
                ValueObject.list.remove(0);
                System.out.println("list size : " + ValueObject.list.size());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
package Thread;

import java.util.ArrayList;
import java.util.List;

public class ValueObject {
    public static List<String> list = new ArrayList<>();
}

我们建立两个线程

package Thread;

public class ThreadAdd extends Thread {
    
    private Add p;
    
    public ThreadAdd(Add p) {
        this.p = p;
    }
    
    @Override
    public void run() {
        p.add();
    }
}
package Thread;

public class ThreadSubtract extends Thread {
    
    private Subtract p;
    
    public ThreadSubtract(Subtract p) {
        this.p = p;
    }
    
    
    @Override
    public void run() {
        p.subtract();
    }
}

我们测试

package Thread;

public class Run {

    public static void main(String[] args) throws InterruptedException {
        
        String lock = new String("");
        Add add = new Add(lock);
        Subtract sub = new Subtract(lock);
        
        ThreadAdd addthread = new ThreadAdd(add);
        
        ThreadSubtract sub1 = new ThreadSubtract(sub);
        sub1.start();
        
        ThreadSubtract sub2 = new ThreadSubtract(sub);
        sub2.start();
        
        Thread.sleep(1000);
        addthread.start();

    }

}

image.png

我们发现发生了异常,这是为什么呢?因为notifyAll同时唤醒了两个减的线程,然后第二个减的线程获得了锁,将size减为0,随后第一个减线程获得锁,再去减就抛异常了,因为它没有继续判断是否为0的条件,所以我们需要在获得锁之后依然去判断条件,也就是将if改为while

package Thread;

public class Subtract {
    private String lock;
    public Subtract(String lock) {
        super();
        this.lock = lock;
    }
    
    public void subtract() {
        try {
            synchronized (lock) {
                while(ValueObject.list.size() == 0) {
                    System.out.println("Wait begin ThreadName:" + Thread.currentThread().getName());
                    lock.wait();
                    System.out.println("Wait end ThreadName:" + Thread.currentThread().getName());
                }
                ValueObject.list.remove(0);
                System.out.println("list size : " + ValueObject.list.size());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

image.png

这样就可以正确运行了。

多个线程等待相同的信号

如果你有多个线程在等待队列中,然后你又要调用notifyAll方法,那么使用while来替代if,是一个很好的解决虚假唤醒的方法。只有一个线程在一个时刻会被唤醒,然后可以获得锁,离开wait方法,并清楚wasSignalled 的标识,一旦这个线程离开了synchronize的语句块,其他线程可以获得锁并且离开wait方法。但是,由于wasSignalled 被第一个线程清除了,其他等待的线程因为while的存在会继续回到wait的状态,知道下一个信号来了

不要对String对象或者全局对象调用wait方法

如果我们对一个String对象调用wait方法

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

如果我们在一个空emptyString或者其他的常量String对象上调用wait方法会产生问题。JVM/Compiler 在内部将常量的String变成相同的对象。这就意味着,即使我们有两个不同的MyWaitNotify实例,他们确实引用着同一个对象。这就意味着本来不相关的两个实例,最后通信的结果可能发生不可预测的交叉结果。 如下图所示:

image.png

需要注意的是,即使四个线程调用wait和notify都是在同一个对象上的,但是信号都是存储在各自的实例中的,也就是wasSignal是存储在各自实例中的,这就会引起很大的问题。一个来自MyWaitNotify 1的信号可能会唤醒MyWaitNotify 2中的等待线程,但是wasSignal确实存在MyWaitNotify 1中的。

如果notify作用在第二个实例上MyWaitNotify 2,那就可能发生线程A和B被唤醒的情况,但是线程A和B会在while循环中检查wasSignal信号,结果发现依然是false,就会继续等待,所以notify并没有起到作用,这就类似虚假唤醒的情况。

这样发生的情况就是,如果我们调用notify方法,然后notify的又不是自己这个实例的线程,结果就没有线程会被唤醒,这就类似于信号丢失的情况。

但如果我们调用的notifyAll方法就不会出现信号丢失的情况,因为wasSignal会被正确的设置,相应的线程会被唤醒,其他对象的线程会因为while循环继续回到wait状态。

那你也许会说,我们直接调用notifyAll不就可以避免String带来的问题么?确实是这样,但是我们如果在全部情况都调用notifyAll的话,就会出现性能的问题,我们完全没有必要在只有一个线程的情况下,调用notifyAll。

所以,我们不要使用全局的对象或者String变量调用wait。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java线程池的工作原理,好处和注意事项

    、 一个线程池管理了一组工作线程, 同时它还包括了一个用于放置等待执行 任务的任务队列(阻塞队列) 。

    desperate633
  • 共享资源的线程安全性Local VariablesLocal Object ReferencesObject Member VariablesThe Thread Control Escape Rul

    如果某段代码可以正确的被多线程并发的执行,那么我们就称这段代码是线程安全的,如果一段代码是线程安全的那么他肯定不会出现资源竞速的问题。资源竞速的问题只发生在多个...

    desperate633
  • Java并发之“饥饿”和“公平锁”(Starvation and Fairness)java中发生线程饥饿的原因java中实现公平锁公平锁性能考虑

    如果一个线程的cpu执行时间都被其他线程抢占了,导致得不到cpu执行,这种情况就叫做“饥饿”,这个线程就会出现饥饿致死的现象,因为永远无法得到cpu的执行。解决...

    desperate633
  • 史上最难的一道Java面试题:分析篇

    无意中了解到如下题目,觉得蛮好。 题目如下: ? 该程序的输出结果? 在java中,多线程的程序最难理解、调试,很多时候执行结果并不像我们想象的那样执行。所以在...

    CSDN技术头条
  • 京东面经汇总

    一、Java Java的优势 平台无关性、垃圾回收 Java有哪些特性,举个多态的例子。 封装、继承、多态 abstract interface区别 含有abs...

    武培轩
  • java大公司后端多线程面试题最强分享

    抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

    好好学java
  • 每天一道面试题 | day11

    sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。

    剑走天涯
  • Thread的wait和notify

    唤醒等待队列中的某个线程,如果时多个线程同时等待并不能指定唤醒某个线程,这有CPU来决定

    DH镔
  • 带你搞懂Java多线程(四)

    等待方: 1.获取对象的锁。 2.检查条件,条件不满足wait 3.条件满足,执行业务代码 syn(对象){ while(条件不满足){ 对象.wa...

    longzeqiu
  • java 为什么wait(),notify(),notifyAll()必须在同步方法/代码块中调用?

    在Java中,所有对象都能够被作为"监视器monitor"——指一个拥有一个独占锁,一个入口队列和一个等待队列的实体entity。所有对象的非同步方法都能够在...

    bear_fish

扫码关注云+社区

领取腾讯云代金券