併發編程系列之線程之間的通信

前言

上節我們介紹了線程從創建到結束的過程,介紹了幾種常見的啓動和終止方法,我們知道了如何使用一個線程,那麼今天我們再接下來看看1個或者多個線程之間是如何進行通信的?OK,讓我們一起走進今天的併發之旅吧,祝您旅途愉快。

景點一:共享變量(volatile)

關鍵字volatile可以用來修飾一個共享的變量字段,使用volatile修飾的變量,當訪問該變量時需要從共享內存中獲取最新值,而對該變量的更新,必須同步刷新回共享內存,這樣就能保證所有線程對該變量訪問的可見性;結合示例代碼更容易理解:

public class RunThread extends Thread{

 private volatile static boolean isRunning = true;
 private void setRunning(boolean isRunning){
   RunThread.isRunning = isRunning;
 }
 
 public void run(){
   System.out.println("進入run方法..");
   while(isRunning == true){
   }
   System.out.println("線程感知isRunning值被設置成false,線程停止!!!");
 }
 
 public static void main(String[] args) throws InterruptedException {
   RunThread runThread1 = new RunThread();
   RunThread runThread2 = new RunThread();
   runThread1.start();
   runThread2.start();
   // 主線程睡眠1秒之後線程1將標識設爲false
   Thread.sleep(1000);
   runThread1.setRunning(false);
   System.out.println("isRunning的值已經被設置了false");
   Thread.sleep(2000);
   System.out.println(runThread1.isAlive());
   System.out.println(runThread2.isAlive());
 }
}

結果:
進入run方法..
進入run方法..
isRunning的值已經被設置了false
線程感知isRunning值被設置成false,線程停止!!!
線程感知isRunning值被設置成false,線程停止!!!
false
false

我們可以看到,開啓2個線程,線程運行終止的條件是isRunning是否爲false,當2個線程都啓動運行之後,主線程睡眠1秒,然後線程1將isRunning設爲false,會發現,線程1和線程2立馬感應到了isRunning值的更新,結束了線程的運行;

當去掉volatile修飾共享變量時,結果如下:2個線程都未感應isRunning的變化。

景點二:同步代碼(synchronized)

synchronized修飾的方法或者代碼塊,主要是確保多個線程在同一時刻,只能有一個線程處於方法或者代碼塊中,保證了線程對變量訪問的可見性和排他性,我們來看下面這段代碼示例:

public class SynchronizedDemo {

   // 方法1 線程1將調用
   public synchronized void method1() {
       try {
           System.out.println(Thread.currentThread().getName() + "執行method1方法,並獲取鎖,主線程睡眠4秒");
           Thread.sleep(4000);
           System.out.println(Thread.currentThread().getName() + "釋放鎖");
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }

   // 方法2 線程2將調用
   public synchronized void method2() {
       System.out.println(Thread.currentThread().getName() + "執行method2方法");
   }

   public static void main(String[] args) throws Exception {
       final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               synchronizedDemo.method1();
           }
       }, "線程1");
       // 爲了保證線程1和2的執行順序我讓線程2延遲0.5秒再啓動
       Thread.sleep(500);
       Thread t2 = new Thread(new Runnable() {
           @Override
           public void run() {
               synchronizedDemo.method2();
           }
       }, "線程2");
       t1.start();
       t2.start();
   }
}

執行結果如下:

當我們把method2方法的synchronized去掉之後,再看執行結果:

從上面示例我們就能看出:當加了synchronized修飾之後線程1首先啓動並持有對象SynchronizedDemo的對象鎖,線程2啓動時,發現鎖被線程1佔用,處於等待狀態,等待3.4秒之後線程1釋放鎖,線程2獲得鎖,執行method2方法。本示例中,synchronized同步的是普通方法,所以持有的是當前實例對象鎖,如果是方法塊,鎖是synchronized包含的代碼塊。

我們去就提到過,synchronized是通過Monitor對象來實現同步的,那麼我們今天來對這點做個補充,我們看下Monitor是如何工作的,如下圖:

總結:任意一個線程對Object對象(synchronized修飾)的訪問,首先要獲得Object對象的監視器,如果獲取失敗,該線程將進入同步隊列等待,該線程狀態變爲阻塞(BLOCKED),當訪問Object的前(已經獲得鎖的線程)線程釋放了鎖,則喚醒阻塞隊列中的線程,使阻塞線程重新嘗試對監視器的獲取;

景點三:等待/通知

等待/通知機制指的是一個線程1調用了對象的wait()方法進入等待狀態,另一個線程2調用該對象的notifyAll()方法,線程1收到了通知之後從對象的wait()方法返回,進而執行後續的操作,兩個線程通過對象來完成交互,而對象上的wait和notify/notifyAll的關係就像一個開關信號一樣,用來完成等待方和通知方之間的交互工作。我們先來看看這幾個方法:

  • notify():通知一個在對象上等待的線程,由WAITING狀態變爲BLOCKING狀態,從等待隊列移動到同步隊列,等待CPU調度獲取該對象的鎖,當該線程獲取到了對象的鎖後,該線程從wait()方法返回;

  • notifyAll():通知所有等待在該對象上的線程,由WAITING狀態變爲BLOCKING狀態,等待CPU調度獲取該對象的鎖;

  • wait():調用該方法的線程進入WAITING狀態,並將當前線程放置到對象的等待隊列,只有等待另外線程的通知或被中斷纔會返回,(需要注意,調用wait()方法後,會釋放對象的鎖);

  • wait(long):超時等待一段時間,參數爲毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回;

  • wait(long,int):第一個參數爲毫秒,第二個參數爲納秒,對超時返回更細粒度的控制;

那麼這幾個方法如何使用呢?我們來看下面2個示例:

public class WaitAndNotifyDemo {
   // 定義一個list屬性
   private volatile static List list = new ArrayList();
   // 提供一個add方法
   public void add() {
       list.add("justin");
   }
   // 獲取list的大小
   public int size() {
       return list.size();
   }

   public static void main(String[] args) {
       final WaitAndNotifyDemo list2 = new WaitAndNotifyDemo();
       final Object lock = new Object();
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   // 使用synchronized對象鎖加鎖
                   synchronized (lock) {
                       System.out.println("t1啓動..");
                       for (int i = 0; i < 6; i++) {
                           // 往list中添加元素,當list中添加了5個元素之後,喚醒等待對象鎖的線程2
                           list2.add();
                           System.out.println("當前線程:" + Thread.currentThread().getName() + "添加了第"+(i+1)+"個元素..");
                           Thread.sleep(500);
                           if (list2.size() == 5) {
                               System.out.println("已經發出通知..");
                               lock.notify();
                           }
                       }
                   }
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }, "t1");

       Thread t2 = new Thread(new Runnable() {
           @Override
           public void run() {
               // synchronized對象鎖加鎖嗎,當list的大小不爲5時,就等待,wait會釋放lock鎖,t1才能獲得鎖
               synchronized (lock) {
                   System.out.println("t2啓動..");
                   if (list2.size() != 5) {
                       try {
                           lock.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                   }
                   System.out.println("當前線程:" + Thread.currentThread().getName() + "收到通知線程停止..");
                   throw new RuntimeException();
               }
           }
       }, "t2");
       // 爲了保證先啓動線程2執行等待,所以我給線程1加了個延遲0.5秒啓動
       t2.start();
       try {
           Thread.sleep(500);
           t1.start();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}

執行結果如下:

我們再看另一個例子,利用等待/通知機制我們來自定義實現一個有界阻塞隊列ArrayBlockingQueue:

public class DefinedArrayBlockingQueue {

   // 定義一個list
   private final LinkedList<Object> list = new LinkedList<Object>();
   // 用於原子操作計數器
   private final AtomicInteger count = new AtomicInteger(0);
   // 隊列最大值和最小值
   private final int maxSize;
   private final int minSize = 0;
   // 用於synchronized對象鎖
   private final Object lock = new Object();

   public DefinedArrayBlockingQueue(int maxSize) {
       this.maxSize = maxSize;
   }

   // 往隊列中加入元素
   public void put(Object obj) {
       synchronized (lock) {
           // 當隊列中元素達到最大值就等待狀態
           while (count.get() == maxSize) {
               try {
                   lock.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           list.add(obj);
           // 計數器+1
           count.getAndIncrement();
           System.out.println(" 元素 " + obj + " 被添加 ");
           // 喚醒take方法的wait
           lock.notify();
       }
   }

   public Object take() {
       Object temp = null;
       synchronized (lock) {
           while (count.get() == minSize) {
               try {
                   lock.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           // 計數器-1
           count.getAndDecrement();
           temp = list.removeFirst();
           System.out.println(" 元素 " + temp + " 被消費 ");
           // 喚醒put方法裏面的wait
           lock.notify();
       }
       return temp;
   }

   public int size() {
       return count.get();
   }

   public static void main(String[] args) throws Exception {
       // 定義一個自定義隊列,最大長度設置爲5
       final DefinedArrayBlockingQueue m = new DefinedArrayBlockingQueue(5);
       m.put("1");
       m.put("2");
       m.put("3");
       m.put("4");
       m.put("5");
       System.out.println("當前元素個數:" + m.size());
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               m.put("6");
               m.put("7");
           }
       }, "t1");

       Thread t2 = new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   Thread.sleep(1000);
                   m.take();
                   Thread.sleep(1000);
                   m.take();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }, "t2");
       t1.start();
       Thread.sleep(1000);
       t2.start();
   }
}

運行結果:

我們再來分析下,等待喚醒機制,並做個總結,整個等待喚醒的過程如下:等待線程首先獲取對象鎖,然後調用對象的wait()方法,此時釋放對象鎖,由於等待線程釋放了對象鎖,喚醒線程隨後獲取到對象鎖,並調用對象的notify()方法,將等待隊列中的線程移到了同步隊列中,此時等待線程狀態的狀態變爲阻塞狀態,喚醒線程釋放了鎖之後,等待線程再次獲取到對象鎖,並從wait()方法返回繼續執行,過程如下圖:

景點四:管道輸入/輸出流

管道輸入輸出流和普通文件的輸入輸出不同之處在於,它是作用於線程之間的數據傳輸,以內存作爲傳輸媒介,主要包括下面4種具體實現:

  • PipedOutputStream(字節)

  • PipedInputStream(字節)

  • PipedReader(字符)

  • PipedWriter(字符)

我們來看下下面這個demo:

public class PipedDemo {

   public static void main(String[] args) throws IOException {
       // 定義一個輸入流
       PipedWriter out = new PipedWriter();
       // 定義一個輸出流
       PipedReader in = new PipedReader();
       // 輸入輸出建立連接
       out.connect(in);
       Thread printThread = new Thread(new PrintThread(in),"打印");
       printThread.start();
       int receive = 0;
       try {
           // 輸入鍵盤字符
           System.out.println("請輸入任意字符,並按enter鍵發送:");
           while ((receive = System.in.read()) != -1) {
               out.write(receive);
           }
       }finally {
           out.close();
       }
   }


   static class PrintThread implements Runnable{
       private PipedReader in;
       // 讀取寫入的字符
       public PrintThread(PipedReader in) {
           this.in = in;
       }

       @Override
       public void run() {
           int receive = 0;
           try {
               while ((receive = in.read()) != -1){
                   System.out.print((char)receive);
               }
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }
}

運行結果如下:

景點五:join方法

如果一個線程執行了thread.join()方法,就說明:當前主線程需要等待執行join方法的線程終止之後才從thread.join()中返回,主線程才繼續往下執行,join主要包括下面3個方法:

  • join源碼:

  • public final void join() throws InterruptedException {
           join(0);
       }

    join超時返回源碼:

    public final synchronized void join(long millis)
       throws InterruptedException {
           long base = System.currentTimeMillis();
           long now = 0;
    
           if (millis < 0) {
               throw new IllegalArgumentException("timeout value is negative");
           }
    
           if (millis == 0) {
               while (isAlive()) {
                   wait(0);
               }
           } else {
               while (isAlive()) {
                   long delay = millis - now;
                   if (delay <= 0) {
                       break;
                   }
                   wait(delay);
                   now = System.currentTimeMillis() - base;
               }
           }
       }
  • join超時返回源碼(更細粒度的控制超時時間):

    public final synchronized void join(long millis, int nanos)
       throws InterruptedException {
           if (millis < 0) {
               throw new IllegalArgumentException("timeout value is negative");
           }
           if (nanos < 0 || nanos > 999999) {
               throw new IllegalArgumentException(
                                   "nanosecond timeout value out of range");
           }
           if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
               millis++;
           }
           join(millis);
       }

 

我們再看下如何使用join:

public class JoinDemo implements Runnable {
       @Override
       public void run() {
           System.out.println("線程"+Thread.currentThread().getName()+":開始運行"+ DateUtil.getNowDate());
           try{
               Thread.sleep(5000);
           }catch(InterruptedException e){
               e.printStackTrace();
           }
           System.out.println("線程"+Thread.currentThread().getName()+":結束運行"+DateUtil.getNowDate());
       }

   public static void main(String[] args) {
       System.out.println("Main Thread Start..."+DateUtil.getNowDate());
       JoinDemo joinDemo = new JoinDemo();
       Thread t1 = new Thread(joinDemo,"t1");
       Thread t2 = new Thread(joinDemo,"t2");
       t1.start();
       t2.start();
   }
}

運行結果如下:

我們使用join之後再看看運行結果會怎樣:

使用join之後我們發現,線程1沒有返回之前,主線程一直在阻塞着,線程2一直沒有啓動,等到5秒線程1運行結束之後,主線程繼續,線程2啓動並執行。

景點六:ThreadLocal本地線程

線程變量,以一個ThreadLocal對象爲鍵,任意對象爲值得存儲結構,一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值,相當於每個線程都有一個獨立的本地對象,這塊內存是線程私有的。對其他線程是不可見的,線程可以利用這塊內存做自己的事,而不會受其他線程干擾。

我們來看下面的demo:

public class ThreadLocalDemo {
   public static ThreadLocal<String> th = new ThreadLocal<String>();
   public void setTh(String value){
       th.set(value);
   }
   public void getTh(){
       System.out.println(Thread.currentThread().getName() + "的值:" + this.th.get());
   }

   public static void main(String[] args) throws InterruptedException {
       final ThreadLocalDemo ct = new ThreadLocalDemo();
       Thread t1 = new Thread(new Runnable() {
           @Override
           public void run() {
               ct.setTh("Justin");
               ct.getTh();
           }
       }, "線程1");

       Thread t2 = new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   Thread.sleep(1000);
                   //ct.setTh("Java");
                   ct.getTh();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       }, "線程2");

       t1.start();
       t2.start();
   }
}

運行結果如下:結果很正常,對於同一個對象,線程1第一次賦值,線程2第二次賦值,分別輸出第一次和第二次的結果。

當我們把線程2賦值註釋起來,按道理是應該會拿到線程1賦的值,但是我們看下面的結果:很明顯線程2拿到個null,說明線程2沒獲取到線程1的值,這相當於,雖然是同一個對象,但是在兩個線程中卻有着不同的副本。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章