前文了解了线程的创建办法和状况切换,在实际开发时,一个进程中往往有很多个线程,大多数线程之间往往不是肯定独立的,比如说咱们需求将A和B 两个线程的履行成果搜集在一起然后显现在界面上,又或许比较典型的顾客-出产者模式,在这些场景下,线程间通讯成了咱们必须运用的手法,那么线程之间怎么通讯呢?

线程间通讯办法,从完结本质来讲,主要可以分为两大类共享内存和音讯传递。

相信咱们还记得,在内存模型一节,咱们说到多线程并发情况下的三大特性,原子性,有序性,可见性,其所对应的解决方案就可以用来完结线程间通讯,这些解决方案的本质便是共享内存。

对于音讯传递而言,最经典的完结便是咱们的Handler机制,在子线程运用主线程的Handler目标将一些信息发送到主线程以便进行处理。

下面咱们来看一些线程间通讯的典型完结

Object.wait/Object.notify

对于Object目标而言,其提供了等候/告诉机制以便完结线程间通讯,因为Object是Java中所有类的父类,也就意味着Java中所有目标都支撑告诉/等候机制,与该机制关联的主要有五个办法:

办法名称 描绘 补白
wait() 线程履行中调用目标的wait办法可以使得当时线程进入WAITING状况,只要等候其他线程的告诉或被中止才会回来,需求留意的是,调用wait办法后,会开释目标的锁 /
wait(long timeout) 与wait意义共同,不同的是经过timeout指定了超时时刻,如果时刻到了还没收到告诉就超时回来 /
wait(long timeout, int nanos) 超时管控愈加精确,第二个参数单位为毫微秒 /
notify 告诉一个在目标上等候的线程使其从wait目标回来 /
notifyAll 告诉所有等候在该目标上的线程 /

以Object.wait/Object.notify完结一个典型的音讯者出产者模型,顾客对变量做-1操作,出产者对变量做+1操作,代码如下:

// 盘子
public class Number {
  // 盘子当时容量,是否有内容
  private int mCount = 0;
 
  //对盘子容量进行+1操作
  public void inc() {
    if (mCount != 0) {
      try {
        this.wait();
       } catch (InterruptedException e) {
        e.printStackTrace();
       }
     }
    mCount++;
    System.out.println(Thread.currentThread().getName()+",mCount+1,mCount="+mCount);
    this.notifyAll();
   }
​
  //对盘子容量进行-1操作
  public void dec() {
    if (mCount == 0) {
      try {
        this.wait();
       } catch (InterruptedException e) {
        e.printStackTrace();
       }
     }
    mCount--;
    System.out.println(Thread.currentThread().getName()+",mCount-1,mCount="+mCount);
    this.notifyAll();
   }
}
  public static void main(String[] args) {
    Number number = new Number();
    // 出产者线程
    Thread incThread = new Thread(new Runnable() {
      @Override
      public void run() {
        number.inc();
       }
     });
    incThread.setName("Inc Thread");
    incThread.start();
​
    // 顾客线程
    Thread decThread = new Thread(new Runnable() {
      @Override
      public void run() {
        number.dec();
       }
     });
    decThread.setName("Dec Thread");
    decThread.start();
   }

如上述代码补白,其间Inc Thread为出产者线程,当盘子内容为0时,每次向盘子Number中放一个内容,顾客线程Dec Thread当盘子有内容时,耗费内容,让盘子内容变为0.运转输出如下:

线程间通信方式(1)

糟糕,正确运转一个循环后,抛出了IllegalMonitorStateException,为什么会这样呢?这个反常是什么意思?

遇事不决看源码,IllegalMonitorStateException的类阐明如下:

Thrown to indicate that a thread has attempted to wait on an object’s monitor or to notify other threads waiting on an object’s monitor without owning the specified monitor.

翻译过来的意思便是当线程在没有持有特定的锁的情况下试图等候目标锁或许告诉其他线程等候目标锁会抛出此反常,有点拗口,先放置,即然咱们调用了wait/notifyAll这两个办法,无妨看下这两个办法的阐明,看是否有新的提示,wait办法阐明如下:

/**
 * Causes the current thread to wait until another thread invokes the
 * {@link java.lang.Object#notify()} method or the
 * {@link java.lang.Object#notifyAll()} method for this object.
 * In other words, this method behaves exactly as if it simply
 * performs the call {@code wait(0)}.
 * <p>
 * The current thread must own this object's monitor. The thread
 * releases ownership of this monitor and waits until another thread
 * notifies threads waiting on this object's monitor to wake up
 * either through a call to the {@code notify} method or the
 * {@code notifyAll} method. The thread then waits until it can
 * re-obtain ownership of the monitor and resumes execution.
 * <p>
 * As in the one argument version, interrupts and spurious wakeups are
 * possible, and this method should always be used in a loop:
 * <pre>
 *   synchronized (obj) {
 *     while (&lt;condition does not hold&gt;)
 *       obj.wait();
 *     ... // Perform action appropriate to condition
 *   }
 * </pre>
 * This method should only be called by a thread that is the owner
 * of this object's monitor. See the {@code notify} method for a
 * description of the ways in which a thread can become the owner of
 * a monitor.
 *
 * @throws  IllegalMonitorStateException  if the current thread is not
 *        the owner of the object's monitor.
 * @throws  InterruptedException if any thread interrupted the
 *       current thread before or while the current thread
 *       was waiting for a notification.  The <i>interrupted
 *       status</i> of the current thread is cleared when
 *       this exception is thrown.
 * @see     java.lang.Object#notify()
 * @see     java.lang.Object#notifyAll()
 */
public final void wait() throws InterruptedException {
  wait(0);
}

在上述阐明中重复说到 The current thread must own this object’s monitor. This method should only be called by a thread that is the owner of this object’s monitor.也便是说在调用Object.wait办法前,当时线程必须持有该目标的锁,获取锁的办法很简单,wait办法阐明中也有,经过synchronized关键词,那么正确的调用代码如下所示:

public class Number {
  private int mCount = 0;
  public void inc() {
    synchronized (this) {
      if (mCount != 0) {
        try {
          this.wait();
         } catch (InterruptedException e) {
          e.printStackTrace();
         }
       }
      mCount++;
      System.out.println(Thread.currentThread().getName()+",mCount+1,mCount="+mCount);
      this.notifyAll();
     }
   }
​
  public void dec() {
    synchronized (this) {
      if (mCount == 0) {
        try {
          this.wait();
         } catch (InterruptedException e) {
          e.printStackTrace();
         }
       }
      mCount--;
      System.out.println(Thread.currentThread().getName()+",mCount-1,mCount="+mCount);
      this.notifyAll();
     }
   }
}

从头运转代码,输出如下:

线程间通信方式(1)

这儿可以看出,只运转了一个循环,那么怎么让它一直运转呢?将if修改称while即可,以出产10次为例,如需一直出产音讯,运用while(true)即可,代码及输出如下:

public class Number {
  private int mCount = 0;
  private int mIncTimes = 0;
  private int mDecTimes = 0;
  public void inc() {
    synchronized (this) {
      while (mIncTimes < 10) {
        if (mCount != 0) {
          try {
            this.wait();
           } catch (InterruptedException e) {
            e.printStackTrace();
           }
         }
        mCount++;
        mIncTimes ++;
        System.out.println(Thread.currentThread().getName()+",mCount+1,mCount="+mCount+",mIncTimes:"+mIncTimes);
        this.notifyAll();
       }
     }
   }
​
  public void dec() {
    synchronized (this) {
      while (mDecTimes < 10) {
        if (mCount == 0) {
          try {
            this.wait();
           } catch (InterruptedException e) {
            e.printStackTrace();
           }
         }
        mCount--;
        mDecTimes++;
        System.out.println(Thread.currentThread().getName()+",mCount-1,mCount="+mCount+",mDecTimes:"+mDecTimes);
        this.notifyAll();
       }
     }
   }
}

线程间通信方式(1)

综上,运用Object.wait/Object.notify/Object.notifyAll时,切记其必须先运用关键词获取同个Object的目标锁,不然就会抛出IllegalMonitorStateException反常

Semaphore

Semaphore翻译为信号量,一个信号量维护一组答应,在调用acquire办法时堵塞,直到获取答应,在调用release的时候开释占用,从而唤醒堵塞的某个线程。信号量操作类似于泊车场车辆办理,初始时泊车场有5个车位,当泊车场内部5个车位全占满时,此刻可用资源为0,即信号量可用答应数量为0,其他车辆想泊车就只能在泊车场外排队堵塞(相当于调用acquire),当一辆车辆从泊车场驶出时(相当于调用release办法),此刻信号量答应数量为1,唤醒一个等候泊车的车辆进入泊车辆,自身可用答应数量再次为0,依此往复。

对于只要一个答应的信号量而言,其可用答应数量为0或1,故被称为二进制信号量,对于有多个正整数可用答应数据的信号量而言,其被称为通用信号量。需求留意在履行acquire时信号量本身并不会持有同步锁,因为这样会影响被开释的答应进入可用答应池中。

二进制信号量,不同于其他锁机制,要求开释锁的线程和获取锁的线程是同一个,也就意味着咱们可以在其他线程开释二进制信号量以完结死锁康复。

下面咱们以二进制信号量完结顾客出产者模式,代码如下(出产消费4次即停止):

public class Counter {
  private int mCount = 0;
  public void incCount() {
    mCount ++;
   }
​
  public void decCount() {
    mCount--;
   }
​
  public int getCount() {
    return mCount;
   }
}
​
// Main主类代码
private static int mIncTimes = 0;
public static void main(String[] args) {
  Counter counter = new Counter();
  Semaphore semaphore = new Semaphore(1);
  Thread incThread = new Thread(new Runnable() {
    @Override
    public void run() {
      while (mIncTimes < 4) {
        try {
          semaphore.acquire();
          if (counter.getCount() == 0) {
            counter.incCount();
            mIncTimes ++;
            System.out.println("Inc Thread ++,current count is:" + counter.getCount());
           }
         } catch (InterruptedException e) {
          e.printStackTrace();
         } finally {
          semaphore.release();
         }
       }
     }
   });
  incThread.setName("Inc Thread");
  incThread.start();
​
  Thread decThread = new Thread(new Runnable() {
    @Override
    public void run() {
      while (mIncTimes < 4) {
        try {
          semaphore.acquire();
          if (counter.getCount() != 0) {
            counter.decCount();
            System.out.println("Dec Thread --,current count is:" + counter.getCount());
           }
         } catch (InterruptedException e) {
          e.printStackTrace();
         } finally {
          semaphore.release();
         }
       }
     }
   });
  decThread.setName("Dec Thread");
  decThread.start();
}

运转成果如下:

线程间通信方式(1)

内存共同性影响,要求一个线程中的release操作和另一个线程中的acquire操作必须存在happen-before联系