Java線程通信(Thread Signaling)

  • 利用共享對象實現(xiàn)通信
  • 忙等(busy waiting)
  • wait(), notify() and notifyAll()
  • 信號丟失(Missed Signals)
  • 虛假喚醒(Spurious Wakeups)
  • 多個線程等待相同的信號
  • 不要對String對象或者全局對象調(diào)用wait方法

線程通信的目的就是讓線程間具有互相發(fā)送信號通信的能力不同。
而且瘸爽,線程通信可以實現(xiàn),一個線程可以等待來自其他線程的信號。舉個例子由蘑,一個線程B可能正在等待來自線程A的信號啊楚,這個信號告訴線程B數(shù)據(jù)已經(jīng)處理好了。

利用共享對象實現(xiàn)通信

一個實現(xiàn)線程通信的簡單的方式就是通過在某些共享的對象變量中設(shè)置一個信號值电谣。舉個例子秽梅,線程A在一個synchronize的語句塊中設(shè)置一個boolean的成員變量hasDataToProcess為true,線程B在一個synchronize語句塊中讀取hasDataToProcess剿牺,如果為true就執(zhí)行代碼企垦,否則就等待。這樣就實現(xiàn)了線程A對線程B的通知晒来〕睿看下面的代碼實現(xiàn):

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執(zhí)行的條件是,等待線程A發(fā)出通知攒读,也就是等到線程A將hasDataToProcess()設(shè)置為true朵诫,所以線程b一直在等待信號,在一個循環(huán)的檢測條件中薄扁。這時候線程B就處于一個忙等的狀態(tài)剪返。废累,因為線程b在等待的過程中是忙碌的,因為線程B在不斷的循環(huán)檢測條件是否成功脱盲。

protected MySignal sharedSignal = ...

...

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

wait(), notify() and notifyAll()

忙等對于cpu的利用不是一個有效率的選擇邑滨,除非忙等的時間是非常短的。不然宾毒,與其讓線程處于忙等的狀態(tài)驼修,不如直接讓線程直接sleep,直到它收到信號再重新激活它诈铛。

Java有一個內(nèi)置的方法乙各,可以讓線程在等待信號的變?yōu)閕nactive狀態(tài)。所有類的超類 java.lang.Object 定義了三個方法幢竹, wait(), notify(), and notifyAll()

一個線程可以對任何一個對象調(diào)用wait方法耳峦,這樣這個線程就會變成wait狀態(tài),inactive焕毫,等待其他線程在同一個對象上調(diào)用notify方法蹲坷,來喚醒這個線程。值得注意的是邑飒,在調(diào)用wait和notify方法之前循签,必須要先獲得這個對象的鎖。換句話說疙咸,線程必須在synchronize的語句塊中調(diào)用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();
    }
  }
}

等待的線程可以調(diào)用dowait方法撒轮,notify線程可以調(diào)用donotify方法乞旦。當(dāng)一個線程在一個對象上調(diào)用notify方法的時候,這個對象的等待線程隊列中的一個線程會被喚醒题山,獲得執(zhí)行的權(quán)利兰粉。notifyAll方法則是會將給定對象的等待隊列中的所有線程都喚醒。

我們可以看到我們調(diào)用wait或者notify方法的時候顶瞳,都是在synchronize語句塊中調(diào)用的玖姑。這是一個必要條件。一個線程如果沒有取得相關(guān)對象的鎖則無法調(diào)用wait和notify方法慨菱,會拋出IllegalMonitorStateException異常焰络。

一旦一個線程調(diào)用wait方法,他就會釋放鎖抡柿,這就允許其他線程去繼續(xù)調(diào)用wait方法或者notify方法舔琅,所以這些方法都必須出現(xiàn)在synchronize語句塊中等恐。

一個線程如果被喚醒了洲劣,不會立即離開wait方法备蚓,因為還沒獲得鎖,要等到那個調(diào)用notify的線程離開他的synchronize的語句塊囱稽,也就是等待他釋放鎖郊尝,才可以獲得鎖,離開wait战惊。換句話說流昏,換句話,線程要離開wait方法吞获,必須重新獲得鎖相應(yīng)對象的鎖况凉。如果多個線程被notifyall方法喚醒,那么在某一個時刻各拷,只有一個被喚醒的線程可以離開wait方法刁绒,因為每個都必須重新獲得鎖才可以離開wait方法。

信號丟失(Missed Signals)

如果在調(diào)用notify或者notifyAll的時候烤黍,線程等待隊列中知市,沒有線程在等待,那么這個喚醒的信號并不會被保存速蕊。而是會丟失嫂丙。所以,如果一個線程在另一個線程調(diào)用wait方法等待之前规哲,就調(diào)用了notify方法跟啤,那么這個notify的信號就被丟失了,這就可能導(dǎo)致那個等待的線程將一直不會被喚醒媳叨,因為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();
    }
  }
}

我們可以看到,上面的方法在調(diào)用notidy之前先將wasSignalled設(shè)置為true痘番。dowait方法會先檢查wasSignalled變量捉片,如果為true,就直接跳過wait方法汞舱,因為已經(jīng)有notify信號發(fā)出了伍纫。如果為false,則說明還沒有信號發(fā)出昂芜,就進(jìn)入wait方法莹规,進(jìn)行等待。所以泌神,我們利用一個boolean變量就可以解決通知過早的問題良漱。

虛假喚醒(Spurious Wakeups)

有時候因為某些原因舞虱,線程可能會在沒有調(diào)用notify或者notifyAll的情況下被喚醒,這也叫做虛假喚醒(Spurious Wakeups)母市。如果一個線程被虛假喚醒就會產(chǎn)生很多意想不到的問題矾兜,所以必須重視這個問題。

我們使用一個自旋鎖機(jī)制患久,也就是用while循環(huán)替代if循環(huán)椅寺,循環(huán)檢查這樣就可以避免虛假喚醒的情況。

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方法現(xiàn)在放在了一個while循環(huán)里蒋失,如果一個線程被喚醒返帕,但是沒有獲得信號,那么wasSignalled 仍是false篙挽,while循環(huán)會進(jìn)行多次判斷溉旋,重新將線程變?yōu)閣ait。

我們更好的理解嫉髓,我們舉一個具體的例子:
假設(shè)有兩個類負(fù)責(zé)加減:

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

我們發(fā)現(xiàn)發(fā)生了異常观腊,這是為什么呢?因為notifyAll同時喚醒了兩個減的線程算行,然后第二個減的線程獲得了鎖梧油,將size減為0,隨后第一個減線程獲得鎖州邢,再去減就拋異常了儡陨,因為它沒有繼續(xù)判斷是否為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

這樣就可以正確運行了骗村。

多個線程等待相同的信號

如果你有多個線程在等待隊列中,然后你又要調(diào)用notifyAll方法呀枢,那么使用while來替代if胚股,是一個很好的解決虛假喚醒的方法。只有一個線程在一個時刻會被喚醒裙秋,然后可以獲得鎖琅拌,離開wait方法,并清楚wasSignalled 的標(biāo)識摘刑,一旦這個線程離開了synchronize的語句塊进宝,其他線程可以獲得鎖并且離開wait方法。但是枷恕,由于wasSignalled 被第一個線程清除了党晋,其他等待的線程因為while的存在會繼續(xù)回到wait的狀態(tài),知道下一個信號來了

不要對String對象或者全局對象調(diào)用wait方法

如果我們對一個String對象調(diào)用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對象上調(diào)用wait方法會產(chǎn)生問題。JVM/Compiler 在內(nèi)部將常量的String變成相同的對象未玻。這就意味著漏益,即使我們有兩個不同的MyWaitNotify實例,他們確實引用著同一個對象深胳。這就意味著本來不相關(guān)的兩個實例,最后通信的結(jié)果可能發(fā)生不可預(yù)測的交叉結(jié)果铜犬。
如下圖所示:

image.png

需要注意的是舞终,即使四個線程調(diào)用wait和notify都是在同一個對象上的,但是信號都是存儲在各自的實例中的癣猾,也就是wasSignal是存儲在各自實例中的敛劝,這就會引起很大的問題。一個來自MyWaitNotify 1的信號可能會喚醒MyWaitNotify 2中的等待線程纷宇,但是wasSignal確實存在MyWaitNotify 1中的夸盟。

如果notify作用在第二個實例上MyWaitNotify 2,那就可能發(fā)生線程A和B被喚醒的情況像捶,但是線程A和B會在while循環(huán)中檢查wasSignal信號上陕,結(jié)果發(fā)現(xiàn)依然是false,就會繼續(xù)等待拓春,所以notify并沒有起到作用释簿,這就類似虛假喚醒的情況。

這樣發(fā)生的情況就是硼莽,如果我們調(diào)用notify方法庶溶,然后notify的又不是自己這個實例的線程,結(jié)果就沒有線程會被喚醒懂鸵,這就類似于信號丟失的情況偏螺。

但如果我們調(diào)用的notifyAll方法就不會出現(xiàn)信號丟失的情況,因為wasSignal會被正確的設(shè)置匆光,相應(yīng)的線程會被喚醒套像,其他對象的線程會因為while循環(huán)繼續(xù)回到wait狀態(tài)。

那你也許會說终息,我們直接調(diào)用notifyAll不就可以避免String帶來的問題么凉夯?確實是這樣,但是我們?nèi)绻谌壳闆r都調(diào)用notifyAll的話采幌,就會出現(xiàn)性能的問題劲够,我們完全沒有必要在只有一個線程的情況下,調(diào)用notifyAll休傍。

所以征绎,我們不要使用全局的對象或者String變量調(diào)用wait。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市人柿,隨后出現(xiàn)的幾起案子柴墩,更是在濱河造成了極大的恐慌,老刑警劉巖凫岖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件江咳,死亡現(xiàn)場離奇詭異,居然都是意外死亡哥放,警方通過查閱死者的電腦和手機(jī)歼指,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甥雕,“玉大人踩身,你說我怎么就攤上這事∩缏叮” “怎么了挟阻?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長峭弟。 經(jīng)常有香客問我附鸽,道長,這世上最難降的妖魔是什么瞒瘸? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任拒炎,我火速辦了婚禮,結(jié)果婚禮上挨务,老公的妹妹穿的比我還像新娘击你。我一直安慰自己,他們只是感情好谎柄,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布丁侄。 她就那樣靜靜地躺著,像睡著了一般朝巫。 火紅的嫁衣襯著肌膚如雪鸿摇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天劈猿,我揣著相機(jī)與錄音拙吉,去河邊找鬼。 笑死揪荣,一個胖子當(dāng)著我的面吹牛筷黔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播仗颈,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼佛舱,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起请祖,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤订歪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后肆捕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刷晋,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年慎陵,在試婚紗的時候發(fā)現(xiàn)自己被綠了眼虱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡荆姆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出映凳,到底是詐尸還是另有隱情胆筒,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布诈豌,位于F島的核電站仆救,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏矫渔。R本人自食惡果不足惜彤蔽,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望庙洼。 院中可真熱鬧顿痪,春花似錦、人聲如沸油够。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽石咬。三九已至揩悄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鬼悠,已是汗流浹背删性。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留焕窝,地道東北人蹬挺。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像它掂,于是被迫代替她去往敵國和親汗侵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容