天天看點

線程間通信

  如果一個多線程程式中每個線程處理的資源沒有交集,沒有依賴關系那麼這是一個完美的處理狀态。你不用去考慮臨界區域(critical section),不用擔心存在所謂的條件競争(race condition),當然也不用去單行執行順序,當然這種狀态隻是完美情況下,事實往往沒有這麼完美。

  當多個線程進入臨界區域對臨界資源進行修改或者讀取的時候,往往需要确定線程的執行順序,以保證共享資源的可見性和相關操作的原子性。這就涉及到線程間的通信了,即

如果線程A正好進入臨界區,他可能對臨界資源進行修改或者讀取,這時候他就要通知随時想要進入臨界區域的線程B:“你丫的等一下,現在隻準我來通路”。我們稱這時候線程A擁有了通路臨界區的鎖。我們可以将鎖看做是一個通行證,擁有鎖的可以在臨界區暢通無阻,而沒有鎖的則需要在門外等着鎖。我們将多個線程的執行過程看做是接力賽,線程A拿着通行證玩遍臨界區之後,還需要将通行證交給下一個想要進入臨界區的線程。當然具體交給誰,你如果純粹交給作業系統來決斷,這就可能産生各種意想不到的後果。極有可能的是剛剛明明決定傳給線程B的,但是就因為線程A多看了線程C一眼,從此就對上了眼,進而把通行證交給了C..... 

  扯得有點遠,不過從上一段我們可以看出線程間最簡單粗暴的通信可以通過加鎖解鎖來實作。最簡單的方式就是synchronized同步塊。如下程式所示:

線程間通信
線程間通信

1 private int count;
2 
3 public synchronized int increment() {
4     return count++;
5 }      

Synchronized 同步塊

     這種說是通信方式,其實說是獨占方式來的更準确些,其實使用synchronized同步塊之後,能夠通路進入臨界區域的隻有一個線程。

    我們考慮另外一種情況,通過信号來實作線程間通信。就像古裝劇裡面,在進攻之前一般會發一些信号,一些等待的線程隻有收到信号改變的時候才會運作,比如下面代碼的這種情況:

線程間通信
線程間通信
1 public class SimpleSignal {
 2     public static void main(String[] args) {
 3         Signal signal = new Signal();
 4         SignalThread t1 = new SignalThread(signal);
 5         SignalThread t2 = new SignalThread(signal);
 6         t1.start();
 7         t2.start();
 8         try {
 9             Thread.sleep(1000);
10         } catch (InterruptedException e) {
11             e.printStackTrace();
12         }
13         signal.start();
14     }
15 }
16 
17 class Signal {
18     private boolean startAction = false;
19 
20     public synchronized void start() {
21         this.startAction = true;
22     }
23 
24     public synchronized boolean isStarted() {
25         return this.startAction;
26     }
27 }
28 
29 class SignalThread extends Thread {
30     private final Signal signal;
31 
32     public SignalThread(Signal signal) {
33         this.signal = signal;
34     }
35 
36     @Override
37     public void run() {
38         while (!signal.isStarted()) {
39             // 什麼也不做,等待可以開始行動
40         }
41 
42         System.out.println("Thread:" + Thread.currentThread()
43                 + " Go Go Go!Fighting!");
44     }
45 
46 }      

SimpleSignal

  上面的代碼可以看出在主線程調用signal.start()之前,線程t1.t2都不會繼續執行,而是阻塞在while循環中等待主線程給出的進攻信号。這中通信實作方式叫做忙等待(busy wait),線程t1和線程t2,一直在while循環判斷條件是否符合,這時候會一直占用CPU處理時間,從CPU使用率上來說不是那麼好。

  那麼又沒有改進方法呢,當然是有的,不必像前面的一樣傻傻的望着天空看是否有信号燈,假如事情順利的話派探子前來告知。在等待的過程中完全可以放棄對CPU的占用,讓CPU去處理其他更加緊急的事情,進而提高CPU的使用率。當有探子來報的時候,CPU則喚醒原來的線程繼續執行。更新版本1.0代碼如下:

線程間通信
線程間通信
1 public class SignalUpV1Test {
 2     public static void main(String[] args) throws InterruptedException {
 3         SignalUpV1 signal = new SignalUpV1();
 4         SignalThreadUpV1 t1 = new SignalThreadUpV1(signal);
 5         SignalThreadUpV1 t2 = new SignalThreadUpV1(signal);
 6         t1.start();
 7         t2.start();
 8         Thread.sleep(1000);
 9         System.out
10                 .println("Now the Main Thread call doNotify of the signal Object!");
11         signal.doNotify();
12     }
13 }
14 
15 class SignalThreadUpV1 extends Thread {
16     private final SignalUpV1 signal;
17 
18     public SignalThreadUpV1(SignalUpV1 signal) {
19         this.signal = signal;
20     }
21 
22     @Override
23     public void run() {
24         try {
25             // 這裡線程等待,給CPU去執行其他事情,然後等着被喚醒
26             signal.doWait();
27         } catch (InterruptedException e) {
28             e.printStackTrace();
29         }
30         System.out.println("Thread" + Thread.currentThread() + " Running");
31     }
32 
33 }
34 
35 class SignalUpV1 {
36     private final Object monitorObject = new Object();
37 
38     public void doWait() throws InterruptedException {
39         // 注意在哪個對象上調用wait或者notify則必須對哪個對象加鎖,而不能對其他對象加鎖,否則會報IllegalMonitorStatus異常
40         synchronized (monitorObject) {
41             monitorObject.wait();
42         }
43     }
44 
45     public void doNotify() {
46         synchronized (monitorObject) {
47             monitorObject.notify();
48         }
49     }
50 }
51 
52 輸出為:
53 Now the Main Thread call doNotify of the signal Object!
54 ThreadThread[Thread-0,5,main] Running      

SignalUpV1

  可以看到線程t1或者t2必須等待主線調用監視對象的doNotify方法才會繼續往下執行,否則會一直等待,當然從輸出結果中也可以看出,doNotify一次隻能喚醒一個線程,程式執行完後JVM還是沒法退出因為有一個線程還是處于等待狀态(要想都喚醒請使用notifyAll而不是notify)。同時還需要注意的一點是Object對象的wait和notify方法,必須在擁有該對象的鎖之後才能調用,否則會報IllegalMonitorStatus異常。

  這種通信方式還是會存在信号丢失的問題(Signal Missing)。即加入調用監視對象的doNotify方法在doWait方法之前,那麼前面等待的線程可能永遠無法被喚醒,解決這種問題的辦法就是加一個标志位,來存儲線程是否已經被喚醒過,線上程調用wait方法之前,判斷線程是否已經被喚醒,如果沒有則調用wait等待喚醒,如果有則不調用wait直接執行。更新版本2.0如下:

線程間通信
線程間通信
1 public class SignalUpV2Test {
 2     public static void main(String[] args) {
 3         SignalUpV2 signal = new SignalUpV2();
 4         SignalThreadUpV2 t1 = new SignalThreadUpV2(signal);
 5         SignalThreadUpV2 t2 = new SignalThreadUpV2(signal);
 6 
 7         // 假設先調用監視對象的doNotify方法
 8         signal.doNotify();
 9         t1.start();
10         t2.start();
11 
12         try {
13             Thread.sleep(1000);
14         } catch (InterruptedException e) {
15             e.printStackTrace();
16         }
17         System.out
18                 .println("Now the main thread call the signal's doNotify method");
19         signal.doNotify();
20     }
21 }
22 
23 class SignalThreadUpV2 extends Thread {
24     private final SignalUpV2 signal;
25 
26     public SignalThreadUpV2(SignalUpV2 signal) {
27         this.signal = signal;
28     }
29 
30     @Override
31     public void run() {
32         try {
33             signal.doWait();
34         } catch (InterruptedException e) {
35             e.printStackTrace();
36         }
37         System.out.println("Thread:" + Thread.currentThread() + " running!");
38     }
39 }
40 
41 class SignalUpV2 {
42     /**
43      * 是否已經被喚醒的标志位。防止先調用doNotify導緻的信号丢失問題進而使線程一直等待被喚醒
44      */
45     private boolean isNotified = false;
46 
47     private final Object monitorObject = new Object();
48 
49     public void doWait() throws InterruptedException {
50         synchronized (monitorObject) {
51             if (!isNotified) {
52                 monitorObject.wait();
53             }
54             this.isNotified = false;
55         }
56     }
57 
58     public void doNotify() {
59         synchronized (monitorObject) {
60             this.isNotified = true;
61             monitorObject.notify();
62         }
63     }
64 }
65 
66 輸出結果:
67 Thread:Thread[Thread-1,5,main] running!
68 Now the main thread call the signal's doNotify method
69 Thread:Thread[Thread-0,5,main] running!      

SignalUpV2

  這個通信版本看起來天衣無縫,事實上在大多數情況下是。但是還有一個不幸的消息,就是作業系統可能無法抑制躁動的心靈。他可能會存在虛假喚醒的情況(Spurious Wakeups)。即存于等待狀态的線程可能無緣無故的被喚醒,進而離開wait方法繼續執行。解決這種問題的辦法很簡單,使用while循環判斷代替if判斷,這樣即使線程被虛假喚醒還是會去校驗喚醒狀态标志位是否為true,如果标志位還是false,會繼續進入wait狀态。進而完美解決了這個問題。實際上這種使用while檢測喚醒辨別位的方式是通過自旋鎖(Spin Lock)來實作的。自旋鎖在處理的過程中不會進行備份然後完全離開線程運作狀态,而是仍然會占用CPU的處理時間,但是不會有線程切換的開銷。更新版本3.0的代碼這裡不給出了,隻需把if改成while即可。

黎明前最黑暗,成功前最絕望!