Basic Of Concurrency(九: 線程通信)

線程通信

線程通信的目的是讓線程之間能夠相互發送信號.更可能是可以讓線程去等待其餘線程的信號.如線程B等待線程A的信號用於指示數據已經準備就緒等待處理.html

經過共享對象通信

一個讓線程通信的簡單方法是經過共享對象來設置信號量.線程A在同步代碼塊中設置布爾類型的成員變量hasDataToProcesstrue.線程B一樣經過同步代碼塊來讀取成員變量hasDataToProcess.下面是一個簡單的例子,讓一個對象包含一個信號量,並提供相應的設置和檢查方法.java

public class MySignal {
    private boolean hasDataToProcess = false;

    public boolean isHasDataToProcess(){
        return this.hasDataToProcess;
    }

    public void setHasDataToProcess(boolean hasDataToProcess){
        this.hasDataToProcess = hasDataToProcess;
    }
}
複製代碼

線程A和B須要有一個指向同一個對象的引用,才能讓信號量正常工做.若是線程A和B引用的不是同一個對象,那麼信號量將沒法正常工做.待處理數據可以獨立於信號量存儲在共享緩衝區中.post

繁忙的等待

線程B處理數據前須要等待數據進入就緒狀態,即等待線程A將信號量hasDataToProcess置換爲true.所以線程B須要在循環中等待信號量:性能

private MySignal sharedSignal= ...
...
while(!sharedSignal.hasDataToProcess()){
	// 繁忙的等待 
}
複製代碼

須要注意的是,當信號量不爲true, 循環會一直進行.咱們稱這種狀況爲繁忙的等待.線程一直在循環等待.this

wait() notify() 和 notifyAll()

若是平均等待時間相對較長,對於計算機cpu來講,繁忙等待並非一個高效的方式.所以讓線程在等待信號的過程當中進入睡眠或閒置狀態是一個不錯的選擇.spa

Java有一套內建的等待機制,用於讓線程在等待信號時進入閒置狀態.java.lang.Object聲明瞭三個方法, wait() notify()notifyAll()用於支持這套機制.線程

一個線程能夠調用任一對象的wait()方法來進入閒置狀態, 其餘對象能夠調用相同對象的notify()方法用於喚醒線程.不管調用對象的wait()仍是notify()方法都先要取得該對象的對象鎖.即須要在同步塊中調用wait()notify().如下是MySignal.class的修改版本,用於示例wait()notify()的使用.code

public class MyWaitNotify {
    private class Monitor{

    }

    private final Monitor monitor = new Monitor();

    public void doWait(){
        synchronized (monitor){
            try {
                monitor.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void doNotify(){
        synchronized (monitor){
            monitor.notify();
        }       
    }
}
複製代碼

等待線程能夠調用doWait(), 通知線程能夠調用doNotify().當有一個線程調用notify(),其餘調用同一對象wait()方法的線程將會被喚醒.調用一次notify()僅會喚醒一個線程.如有多個線程調用了同一對象的wait()方法, 能夠經過調用notifyAll()來喚醒所有線程.cdn

你能看到等待和通知線程在調用wait()notify()時是在同步塊中進行的, 而這是必須的.若在沒有得到任意對象的對象鎖前提下, 調用該對象wait() notify()notifyAll()中的任一方法都會拋出IllegalMonitorStateException異常.htm

當等待線程在同步代碼塊中執行過程當中, 會不會一直取得對象鎖不放, 這樣其餘通知線程就不能進入同步代碼塊來調用notify()方法了.答案是否認的, 一旦等待線程調用wait()方法, 會連同對象鎖一塊兒釋放, 對notify()的調用也是如此.這樣就能讓其餘線程調用wait()notify()方法了, 前提是他們都在同步代碼塊中調用.

當一個線程被喚醒時並不能立刻離開wait()方法,還須要等待通知線程調用notify()完畢後離開同步代碼塊釋放鎖.換句話說,被喚醒的線程須要得到對象鎖才能退出wait()方法,由於wait()方法是在同步代碼塊中調用的.若是多個線程經過notifyAll()方法被喚醒, 那麼同一時刻只會有一個線程退出wait()方法,由於其餘線程退出wait()方法必須得到對象鎖.

信號丟失

若沒有任何線程調用相同對象wait()方法前提下, 調用notify()notifyAll(), 通知信號將不會存儲. 此時信號將會丟失. 若在等待線程調用wait()方法以前就調用了notify()方法, 那麼對於等待線程來講, 信號已經丟失了.固然這並非什麼大問題, 但有些時候, 這將會致使等待線程無限期的等待下去, 由於喚醒信號已經丟失了.

爲了防止這種狀況發生, 咱們須要將信號量存儲起來.咱們能夠在上文MyWaitNotify.class上進行改進,在該類中置放一個成員變量,用於存儲信號量.

public class MyWaitNotify2 {
    private class Monitor{

    }

    boolean wasSignalled = false;

    private final Monitor monitor = new Monitor();

    public void doWait(){
        synchronized (monitor){
            if(!wasSignalled){
                try {
                    monitor.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            wasSignalled = false;
        }
    }

    public void doNotify(){
        synchronized (monitor){
            wasSignalled = true;
            monitor.notify();
        }
    }
}
複製代碼

如今能夠注意到doNotify()方法中在調用監控對象的notify()方法前, 先置換wasSignalledtrue.一般在doWait()方法中, 在調用監控對象的wait()方法前先檢查wasSignalled的值是否爲false.若不是,則不調用wait()方法,置換wasSignalledfalse.這樣等待線程只有在上一次調用doWait()和這一次中間沒有接收到任何信號量的前提下才會調用監控對象的wait()方法.

虛假喚醒

出於一些未知的緣由,線程在沒有調用notify()notifyAll()的前提下也會被喚醒.出於未知緣由的喚醒, 咱們稱之爲虛假喚醒.

若是上文MyWaitNotify2.class示例中,等待線程在doWait()方法中發生虛假喚醒,線程將在沒有接收到預期信號量的前提下執行, 這將會在你的應用中產生若干問題.

爲了防止虛假喚醒的發生, 咱們須要用while()循環檢查信號量來代替if語句.這種狀況咱們稱之爲旋轉鎖.只要在旋轉條件不成立時, 旋轉鎖纔會釋放.一樣咱們在上文MyWaitNotify2.class上進行改進:

public class MyWaitNotify3 {
    private class Monitor{

    }

    boolean wasSignalled = false;

    private final Monitor monitor = new Monitor();

    public void doWait(){
        synchronized (monitor){
            while(!wasSignalled){
                try {
                    monitor.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            wasSignalled = false;
        }
    }

    public void doNotify(){
        synchronized (monitor){
            wasSignalled = true;
            monitor.notify();
        }
    }
}
複製代碼

咱們能看到, 僅僅是將原先的if語句替換爲while. 等待線程若是在沒有接收到信號量的前提下喚醒, 將會進入旋轉鎖中, 從新調用wait()進入等待狀態. 等待線程只有在接收到信號量後纔會釋放旋轉鎖.

多個線程等待同一信號量

當使用notifyAll()喚醒在等待同一個信號量的多個線程且僅容許它們中一個繼續運行時, while循環是一個不錯的解決方案.此時只有一個線程可以得到對象鎖, 即只有一個線程可以退出wait()方法並清除wasSignalled標記.一旦該線程退出doWait()方法中的同步塊,其餘線程能夠得到對象鎖退出wait()方法, 而後進入while()檢查wasSignalled成員變量.此時wasSignalled標記已經被上一個喚醒的線程清除, 當前線程只能在片刻喚醒後從新進入等待狀態,直到下一個信號量到來.

不要在String常量和全局對象上調用wait()方法

將上文中的MyWaitNotify3.class示例稍做修改,使用一個空String做爲監控對象.

done like this:

public class MyWaitNotify4 {
    boolean wasSignalled = false;

    private final String monitor = "";

    public void doWait(){
        synchronized (monitor){
            while(!wasSignalled){
                try {
                    monitor.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            wasSignalled = false;
        }
    }

    public void doNotify(){
        synchronized (monitor){
            wasSignalled = true;
            monitor.notify();
        }
    }
}
複製代碼

在空String或其餘String常量上調用wait()notify(), JVM編譯器內部會將String常量轉換爲同一個對象.這意味着, 即便你有兩個徹底不一樣的MyWaitNotify實例, 它們各自的monitor成員變量都會指向同一個空String對象.這也意味着調用第一個MyWaitNotify實例doWait()方法的線程可能會被調用第二個MyWaitNotify實例doNotify()方法的線程喚醒.

以上狀況能夠用下圖歸納:

記住,即便4個線程都是調用同一個共享String對象的wait()notify()方法,調用doWait()doNotify()所產生的信號量任然分別存儲在不一樣的MyWaitNotify實例中.一個線程調用MyWaitNotify1doNotify()方法可能會喚醒等待MyWaitNotify2的線程, 但產生的信號量任然會存儲在MyWaitNotify1實例中.

這看起來好像沒什麼大問題,但若是第二個MyWaitNotify實例的doNotify()被調用, 意外喚醒了線程A和線程B, 此時線程A和B會在旋轉鎖中檢查信號量標記後從新進入等待狀態,由於第一個MyWaitNotify實例的doNotify()方法並無被調用.信號量標記任然是false.這種狀況相似於激活了虛假喚醒.線程A或B並無得到信號量.但旋轉鎖能夠處理這樣的狀況,所以線程A或B會從新進入等待狀態.

問題在於doNotify()中調用的是notify(),四個等待同個空String對象鎖的線程僅會有一個能被喚醒,可能被喚醒的線程並非給定想要傳遞信號量的目標線程.若是A和B其中一個線程被喚醒,而信號量實際是給C或D的,此時A和B進入while()檢查信號量標記, 發現爲false,則從新回到等待狀態.而此時C和D並無被喚醒,而信號量卻到達了,所以信號量此刻丟失了.這種狀況相似於前文提到的信號丟失.C和D收到了信號量卻沒有正確響應它.

若把doNotify()中的notify()換成notifyAll()則全部的線程都會被喚醒並根據信號量做出響應.線程A和B仍然會進入等待狀態,但C和D其中有一個可以正確響應信號量,由於它發現信號量被置換爲true則退出wait()方法響應信號量並清除信號量標記.另外一個則檢查到清除後的信號量從新回到等待狀態.

你或許會傾向於使用notifyAll()來代替notify(),但實際這會帶來額外的性能損耗,並非最佳選擇.沒有理由喚醒全部的線程來響應一個信號量.

因此千萬不要調用全局對象或String常量的wait()notify()方法.而是使用一個獨一無二的實例對象.就像MyWaitNotify3.class示例中,使用Monitor實例來代替空String.

該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: ThreadLocal
下一篇: 死鎖和預防

相關文章
相關標籤/搜索