[Java多線程編程之五] 線程通信

??在多線程環(huán)境中,多個線程之間互相協(xié)作叮雳,以達到高效實現(xiàn)程序功能的目的想暗,比如某些多線程程序要求線程執(zhí)行有先后順序、獲取某個線程的執(zhí)行結果帘不,要想實現(xiàn)多個線程之間的協(xié)同江滨,就需要線程之間互相通信,線程通信主要分為一下四類:

  • 1)文件共享
  • 2)網(wǎng)絡共享
  • 3)共享變量
  • 4)JDK提供的線程協(xié)調API(主要有:suspend/resume厌均、wait/notify唬滑、park/unpark


一、文件共享

??一個線程將數(shù)據(jù)寫入到文件中,另一個線程再去讀取文件晶密,實現(xiàn)數(shù)據(jù)的共享擒悬,最終達到線程通信的目的。



代碼示例:

public class FileShareComm {
    public static void main(String[] args) {
        // 線程1 - 寫入數(shù)據(jù)
        new Thread(() -> {
            System.out.println("線程1啟動");
            try {
                while (true) {
                    Files.write(Paths.get("data.log"), 
                                ("當前時間" + String.valueOf(System.currentTimeMillis())).getBytes());
                    Thread.sleep(1000L);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start(); 
        
        // 線程2 - 讀取數(shù)據(jù)
        new Thread(() -> {
            System.out.println("線程2啟動");
            try {
                while (true) {
                    Thread.sleep(1000L);
                    byte[] allBytes = Files.readAllBytes(Paths.get("data.log"));
                    System.out.println(new String(allBytes));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }
}

程序運行結果:

線程1啟動
線程2啟動
當前時間1570711321681
當前時間1570711322768
當前時間1570711323768
當前時間1570711324775


二稻艰、網(wǎng)絡共享

??通俗地說就是網(wǎng)絡上不同計算機之間通過套接字(Socket)進行通信懂牧,一個Socket一般由IP和Port組成。



三尊勿、共享變量

??多個線程對某個內存中數(shù)據(jù)進去讀取和寫入僧凤,實現(xiàn)線程通信。



代碼示例:

public class VariableShareComm {
    // 共享變量
    public static String content = "空";
    
    public static void main(String[] args) {
        // 線程1 - 寫入數(shù)據(jù)
        new Thread(() -> {
            System.out.println("線程1啟動!");
            try {
                while (true) {
                    content = "當前時間" + String.valueOf(System.currentTimeMillis());
                    Thread.sleep(1000L);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
        
        // 線程2 - 讀取數(shù)據(jù)
        new Thread(() -> {
            System.out.println("線程2啟動!");
            try {
                while (true) {
                    Thread.sleep(1000L);
                    System.out.println(content);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }
}

程序運行結果:

線程1啟動!
線程2啟動!
當前時間1570712442853
當前時間1570712443859
當前時間1570712444868
當前時間1570712445883


四元扔、線程協(xié)作 — JDK API

??JDK中對于需要多線程協(xié)作完成某一任務的場景躯保,提供了對應API支持,主要有suspend/resume澎语、wait/notify途事、park/unpark
??關于多線程協(xié)作有個經(jīng)典的場景:生產(chǎn)者 - 消費者模型(線程阻塞擅羞、線程喚醒)
示例:線程1去買包子尸变,沒有包子,則暫停執(zhí)行减俏,等待通知召烂;線程2生產(chǎn)出包子,通知線程1繼續(xù)執(zhí)行娃承。

生產(chǎn)者-消費者模型

??下面演示如何用各個JDK API實現(xiàn)生產(chǎn)者-消費者模型奏夫。

1、被棄用的suspend和resume

??調用suspend掛起目標線程草慧,通過resume可以恢復線程執(zhí)行桶蛔,由于supend/resume即要求resumesuspend之后調用,并且suspend被調用后不會釋放鎖漫谷,因此容易寫出死鎖的代碼仔雷,所以被棄用。

(1)死鎖的場景1:suspend不釋放鎖舔示,resume需要獲取鎖

代碼示例:

public class Demo6 {
    public static Object baozidian = null;   // 包子店

    /** 死鎖的suspend/resume: suspend并不會像wait一樣釋放鎖碟婆,因此容易寫出死鎖代碼 */
    public void suspendResumeDeadLockTest() throws Exception {
        // 啟動線程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) {    // 如果沒包子现使,則進入等待
                System.out.println("1纳击、進入等待");
                // 當前線程拿到鎖淀衣,然后掛起(還是RUNNABLE狀態(tài))
                synchronized (this) {
                    Thread.currentThread().suspend();
                }
            }
            System.out.println("2植锉、買到包子,回家");
        });
        consumerThread.start();
        // 3秒之后潘懊,生產(chǎn)一個包子
        Thread.sleep(3000L);
        //System.out.println("consumerThread's status " + consumerThread.getState().toString());
        baozidian = new Object();
        synchronized (this) {
            consumerThread.resume();
        }
        System.out.println("3基协、通知消費者");
    }

    public static void main(String[] args) throws Exception {
        Demo6 demo6 = new Demo6();
        demo6.suspendResumeDeadLockTest();
    }
}

執(zhí)行結果:


【代碼解析】
??由于resume會在休眠3秒之后被調用洋措,所以保證了resume在suspend之后執(zhí)行,consumerThread的run方法體內執(zhí)行suspend之前要先拿到demo6對象鎖淌铐,3秒后創(chuàng)建baozidian對象肺然,主線程要調用resume方法通知consumerThread線程,但是由于執(zhí)行suspend時沒有釋放demo5對象鎖腿准,所以這里主線程沒辦法拿到鎖际起,導致沒法執(zhí)行resume,結果是consumerThread永遠處于掛起狀態(tài)吐葱。

(2)死鎖的場景2:resume在suspend之前執(zhí)行

代碼示例:

    /** 導致永久掛起的suspend/resume */
    public void suspendResumeDeadLockTest2() throws Exception {
        // 啟動線程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) {    // 如果沒包子街望,則進入等待
                System.out.println("1、進入等待");
                try {
                    Thread.sleep(5000L);  // 為這個線程加上一點延時    
                } catch (Exception e) {
                    e.printStackTrace();
                }   
                // 這里的掛起執(zhí)行在resume后面
                Thread.currentThread().suspend();
            }
            System.out.println("2弟跑、買到包子灾前,回家");
        });
        consumerThread.start();     
        // 3秒之后,生產(chǎn)一個包子
        Thread.sleep(3000L);
        baozidian = new Object();
        consumerThread.resume();
        System.out.println("3窖认、通知消費者");
        consumerThread.join();
    }

執(zhí)行結果:


【代碼解析】
??由于consumerThread的run方法體內執(zhí)行suspend之前會先休眠5秒豫柬,所以導致resume會先執(zhí)行告希,suspend后執(zhí)行扑浸,后面的程序不會再次resume,同樣會導致consumerThread永遠處于掛起狀態(tài)燕偶。

(3)正常的suspend/resume

代碼示例:

    /** 正常的suspend/resume */
    public void suspendResumeTest() throws Exception {
        // 啟動線程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) {    // 如果沒包子喝噪,則進入等待
                System.out.println("1、進入等待");
                Thread.currentThread().suspend();
            }
            System.out.println("2指么、買到包子酝惧,回家");
        });
        consumerThread.start();
        // 3秒之后,生產(chǎn)一個包子
        Thread.sleep(3000L);    // 延遲3秒伯诬,保證調用resume()之前已經(jīng)調用suspend()完畢
        baozidian = new Object();
        consumerThread.resume();
        System.out.println("3晚唇、通知消費者");
    }

執(zhí)行結果:



2、wait/notify機制

??wait/notify依賴于Java對象監(jiān)視器鎖盗似,而監(jiān)視器鎖又是跟sychronized配合使用的哩陕,因此wait/notify必須寫在同步塊中,并且wait/notify方法只能由同一對象鎖的持有者線程調用赫舒,否則會拋出IllegalMonitorStateException異常悍及。
??特別注意,使用sychronized時接癌,用到的監(jiān)視器鎖是監(jiān)視對象obj對應的監(jiān)視器鎖心赶,所以調用wait方法時,必須調用obj.wait()缺猛,這樣obj的對象監(jiān)視器才會去釋放對應的監(jiān)視器鎖缨叫。

synchronized (obj) {                    
    try {
        System.out.println("1椭符、進入等待");
        obj.wait();
    } catch (InterruptedException e) {                      
        e.printStackTrace();
    }
}
(1)正常的wait/notify

??waitnotify方法都必須在sychronized塊中被調用,且sychronized和調用方法時必須使用相同的鎖對象耻姥,notify必須在wait被調用之后再調用艰山,代碼如下:

public class Demo6 {
    public static Object baozidian = null;   // 包子店
    /** 正常的wait/notify */
    public void waitNotifyTest() throws Exception {
        // 啟動線程
        new Thread(() -> {
            if (baozidian == null) {
                synchronized (this) {
                    try {
                        System.out.println("1、進入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2咏闪、買到包子曙搬,回家");
        }).start();
        // 3秒之后,生產(chǎn)一個包子
        Thread.sleep(3000L);
        baozidian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3鸽嫂、通知消費者");
        }
    }
    public static void main(String[] args) throws Exception {
        Demo6 demo6 = new Demo6();
    
        // 2纵装、wait/notify
        demo6.waitNotifyTest();
    }
}

執(zhí)行結果:



(2)死鎖的wait/notify

??wait方法會導致當前線程等待,加入對應的對象的監(jiān)視器等待集合中据某,并且釋放當前持有的對象鎖橡娄,notify/notifyAll方法會喚醒一個或者所有正在等待該對象鎖的線程。
??雖然wait會自動解鎖癣籽,但是對調用順序有要求挽唉,如果在notify被調用之后,才開始wait方法的調用筷狼,線程會永遠處于WAITING狀態(tài)瓶籽;如果調用notify/notifyAll時對象鎖的等待集合中沒有等待的線程,自然通知不到任一個線程埂材,只有在通知前有線程調用了wait進入等待集合中塑顺,才能真正通知到等待的線程。

代碼示例:

public class Demo6 {
    public static Object baozidian = null;   // 包子店
    /** 會導致程序永久等待的wait/notify */
    public void waitNotifyDeadLockTest() throws Exception {
        // 啟動線程
        new Thread(() -> {
            if (baozidian == null) {
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e) {                  
                    e.printStackTrace();
                }
                synchronized (this) {                   
                    try {
                        System.out.println("1俏险、進入等待");
                        this.wait();
                    } catch (InterruptedException e) {                      
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2严拒、買到包子,回家");
        }).start();
        // 3秒之后竖独,生產(chǎn)一個包子
        Thread.sleep(3000L);
        baozidian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3裤唠、通知消費者");
        }
    }
    public static void main(String[] args) throws Exception {
        Demo6 demo6 = new Demo6();
        
        // 2、wait/notify        
        demo6.waitNotifyDeadLockTest();
    }
}

執(zhí)行結果:



3莹痢、park/unpark機制

??park/unpark是JDK API的另一種線程通信機制种蘸,一個線程調用了park則等待頒發(fā)一個“許可”,如果當前存在未被使用的許可格二,則線程可以直接獲取許可直接運行劈彪;如果當前許可數(shù)為0,則需要等待其他線程調用unpark頒發(fā)許可顶猜。
??需要許可和頒發(fā)許可的線程沒有強依賴關系沧奴,任何一個線程頒發(fā)的許可都可以被任意需要許可的線程使用,任何一個線程都可以頒發(fā)許可长窄。因此滔吠,parkunpark對調用順序沒有要求纲菌,同時由于park/unpark不像wait/notify那樣是基于鎖監(jiān)視器的,所以park/unpark不會釋放當前線程持有的鎖疮绷。
??一個線程多次調用park時翰舌,只有第一次調用生效,不會因為多次park而去獲取多個許可證冬骚,因為底層是基于一個布爾值的CAS原子操作椅贱。

(1)正常的park/unpark

代碼示例:

public class Demo6 {
    public static Object baozidian = null;   // 包子店
    /** 正常的park/unpark */
    public void parkUnparkTest() throws Exception {
        // 啟動線程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) {
                System.out.println("1、進入等待");
                LockSupport.park();             
            }
            System.out.println("2只冻、買到包子庇麦,回家");
        });
        consumerThread.start();
        // 3秒之后,生產(chǎn)一個包子
        Thread.sleep(3000L);
        baozidian = new Object();
        System.out.println();
        LockSupport.unpark(consumerThread);
        System.out.println("3喜德、通知消費者");
    }
    public static void main(String[] args) throws Exception {
        Demo6 demo6 = new Demo6();
        
        // 3山橄、park/unpark
        demo6.parkUnparkTest();
    }
}

執(zhí)行結果:



(2)死鎖的park/unpark

??由于park時不會釋放鎖,所以如果執(zhí)行park所在的代碼塊是需要先獲取鎖的同步代碼塊舍悯,并且unpark()需要獲取相同的鎖時航棱,會觸發(fā)死鎖。
代碼示例:

public class Demo6 {
    public static Object baozidian = null;   // 包子店
    /** 死鎖的park/unpark */
    public void parkUnparkDeadLockTest() throws Exception {
        // 啟動線程
        Thread consumerThread = new Thread(() ->{
            if (baozidian == null) {
                System.out.println("1萌衬、進入等待");
                synchronized (this) {
                    LockSupport.park();
                }
            }
            System.out.println("2饮醇、買到包子,回家");
        });
        consumerThread.start();
        // 3秒之后奄薇,生產(chǎn)一個包子
        Thread.sleep(3000L);
        baozidian = new Object();
        synchronized (this) {
            LockSupport.unpark(consumerThread);
        }
        System.out.println("3驳阎、通知消費者");
    }
    public static void main(String[] args) throws Exception {
        Demo6 demo6 = new Demo6();
        
        // 3抗愁、park/unpark
        demo6.parkUnparkDeadLockTest();
    }
}


4馁蒂、總結

通訊方式 等待通知時狀態(tài) 死鎖場景 優(yōu)點 缺點
suspend/resume RUNNABLE 1、resume在suspend之前被調用
2蜘腌、suspend和resume所在的同步代碼塊使用相同的鎖導致死鎖
~ 很容易觸發(fā)死鎖
wait/notify WAITING notify在wait之前使用 基于對象監(jiān)視器鎖沫屡,調用wait時會釋放鎖 執(zhí)行順序有要求
park/unpark WAITING park和unpark所在同步代碼塊使用相同的鎖導致死鎖 對執(zhí)行順序沒要求 park不會釋放鎖,可能導致死鎖


五撮珠、偽喚醒

??一般情況下沮脖,當線程運行需要的等待某個條件還不具備是,線程會調用上述的suspend芯急、wait勺届、park方法將線程掛起,然后等待另一個線程滿足這個條件后再通知掛起線程娶耍,如果使用if語句來判斷是否進入等待狀態(tài)免姿,可能會引起偽喚醒問題,問題代碼模板示例:

sychronized(lock) {
    if (<條件判斷>) {
        lock.wait();
    }
    // 執(zhí)行后續(xù)操作
}
1榕酒、什么是偽喚醒胚膊?

??偽喚醒是指線程并非因為notify故俐、notifyAllunpark等api調用而喚醒紊婉,是更底層的原因導致的药版,此時條件判斷還不滿足,但是卻因為偽喚醒運行后續(xù)的代碼喻犁,導致程序運行異巢燮或錯誤。

2肢础、如何解決偽喚醒問題筐乳?

??不用if語句來判斷,而是在循環(huán)中檢查等待條件乔妈,這樣確保程序在偽喚醒的條件下依然不會在條件沒滿足的情況下去執(zhí)行后續(xù)操作蝙云,而是再次將線程掛起,如下所示:

// wait
sychronized(obj) {
    while (<條件判斷>) {
        obj.wait();
    }
    // 執(zhí)行后續(xù)操作
}
// park
while(<條件判斷>) {
    LockSupport.park();
    // 執(zhí)行后續(xù)操作
}

實例代碼演示:

    /** 正常的wait/notify */
    public void waitNotifyTest() throws Exception {
        // 啟動線程
        Thread consumerThread = new Thread(() -> {
            while (baozidian == null) {
                synchronized (this) {
                    try {
                        System.out.println("1路召、進入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2勃刨、買到包子,回家");
        });
        consumerThread.start();
        // 3秒之后股淡,生產(chǎn)一個包子
        Thread.sleep(3000L);
        System.out.println("consumerThread's status " + consumerThread.getState().toString());
        baozidian = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3身隐、通知消費者");
        }
    }

    /** 正常的park/unpark */
    public void parkUnparkTest() throws Exception {
        // 啟動線程
        Thread consumerThread = new Thread(() -> {
            while (baozidian == null) {
                System.out.println("1、進入等待");
                LockSupport.park();             
            }
            System.out.println("2唯灵、買到包子贾铝,回家");
        });
        consumerThread.start();
        // 3秒之后,生產(chǎn)一個包子
        Thread.sleep(3000L);
        baozidian = new Object();
        //System.out.println(consumerThread.getState().toString());
        LockSupport.unpark(consumerThread);
        System.out.println("3埠帕、通知消費者");
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末垢揩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子敛瓷,更是在濱河造成了極大的恐慌叁巨,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件呐籽,死亡現(xiàn)場離奇詭異锋勺,居然都是意外死亡,警方通過查閱死者的電腦和手機狡蝶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評論 3 392
  • 文/潘曉璐 我一進店門庶橱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贪惹,你說我怎么就攤上這事苏章。” “怎么了馍乙?”我有些...
    開封第一講書人閱讀 163,105評論 0 353
  • 文/不壞的土叔 我叫張陵布近,是天一觀的道長垫释。 經(jīng)常有香客問我,道長撑瞧,這世上最難降的妖魔是什么棵譬? 我笑而不...
    開封第一講書人閱讀 58,242評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮预伺,結果婚禮上订咸,老公的妹妹穿的比我還像新娘。我一直安慰自己酬诀,他們只是感情好脏嚷,可當我...
    茶點故事閱讀 67,269評論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瞒御,像睡著了一般父叙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肴裙,一...
    開封第一講書人閱讀 51,215評論 1 299
  • 那天趾唱,我揣著相機與錄音,去河邊找鬼蜻懦。 笑死甜癞,一個胖子當著我的面吹牛,可吹牛的內容都是我干的宛乃。 我是一名探鬼主播悠咱,決...
    沈念sama閱讀 40,096評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼征炼!你這毒婦竟也來了析既?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,939評論 0 274
  • 序言:老撾萬榮一對情侶失蹤柒室,失蹤者是張志新(化名)和其女友劉穎渡贾,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雄右,經(jīng)...
    沈念sama閱讀 45,354評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,573評論 2 333
  • 正文 我和宋清朗相戀三年纺讲,在試婚紗的時候發(fā)現(xiàn)自己被綠了擂仍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,745評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡熬甚,死狀恐怖逢渔,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情乡括,我是刑警寧澤肃廓,帶...
    沈念sama閱讀 35,448評論 5 344
  • 正文 年R本政府宣布智厌,位于F島的核電站,受9級特大地震影響盲赊,放射性物質發(fā)生泄漏铣鹏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,048評論 3 327
  • 文/蒙蒙 一哀蘑、第九天 我趴在偏房一處隱蔽的房頂上張望诚卸。 院中可真熱鬧,春花似錦绘迁、人聲如沸合溺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽棠赛。三九已至,卻和暖如春膛腐,著一層夾襖步出監(jiān)牢的瞬間恭朗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評論 1 269
  • 我被黑心中介騙來泰國打工依疼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留痰腮,地道東北人。 一個月前我還...
    沈念sama閱讀 47,776評論 2 369
  • 正文 我出身青樓律罢,卻偏偏與公主長得像膀值,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子误辑,可洞房花燭夜當晚...
    茶點故事閱讀 44,652評論 2 354

推薦閱讀更多精彩內容

  • ??談到并發(fā)我們就會想到多線程沧踏,要想實現(xiàn)多個線程之間的協(xié)同,如:線程執(zhí)行先后順序巾钉、獲取某個線程執(zhí)行的結果等等翘狱。都涉...
    TodoCoder閱讀 535評論 0 4
  • 線程通信的方法 程序在使用多線程執(zhí)行任務時,經(jīng)常需要線程之間協(xié)同工作砰苍。此時潦匈,我們需要了解線程通信的手段。 線程通信...
    疊最厚的甲閱讀 734評論 0 0
  • 線程通信 線程通信指的是多個線程在運行的期間赚导,相互之間的數(shù)據(jù)交互協(xié)作茬缩。 1.通信方式 實現(xiàn)多個線程直接的協(xié)作,涉及...
    JuneWool閱讀 284評論 0 0
  • 要想實現(xiàn)多個線程之間的協(xié)同吼旧,如:線程執(zhí)行先后順序凰锡、獲取某個線程執(zhí)行的結果等等。涉及到線程之間相互通信,分為下面四類...
    JavaEdge閱讀 588評論 0 5
  • 參考資料:《Java高并發(fā)程序設計》 1.同步控制 1.擴展了synchronized功能的:重入鎖 1.簡介 使...
    agile4j閱讀 887評論 0 0