java线程间通信:一个小Demo完全搞懂
版權(quán)聲明:本文出自汪磊的博客,轉(zhuǎn)載請務(wù)必注明出處。
Java線程系列文章只是自己知識的總結(jié)梳理,都是最基礎(chǔ)的玩意,已經(jīng)掌握熟練的可以繞過。
一、從一個小Demo說起
上篇我們聊到了Java多線程的同步機(jī)制:Java多線程同步問題:一個小Demo完全搞懂。這篇我們聊一下java多線程之間的通信機(jī)制。
上一篇探討java同步機(jī)制的時候我們舉得例子輸出log現(xiàn)象是:一段時間總是A線程輸出而另一段時間總是B線程輸出,有沒有一種方式可以控制A,B線程交錯輸出呢?答案是當(dāng)然可以了,這時候我們就要用到多線程的wait/notify機(jī)制了。
wait/notify機(jī)制就是當(dāng)線程A執(zhí)行到某一對象的wait()方法時,就會進(jìn)入等待狀態(tài),此時線程A放棄持有的鎖,其余線程可以競爭鎖的持有權(quán)。當(dāng)有其余線程調(diào)用notify()或者notifyAll()方法的時候就可能(當(dāng)有多個線程的時候notify()方法只會喚醒處于等待狀態(tài)線程中的一個)喚醒線程A,使其從wait狀態(tài)醒來,繼續(xù)向下執(zhí)行業(yè)務(wù)邏輯。
接下來,我們通過一個小demo加以理解。
二、單生產(chǎn)者消費者模式
demo很簡單,就是開啟兩個線程,一個生產(chǎn)面包,另一個負(fù)責(zé)消費面包,并且生產(chǎn)一個就要消費一個,交替執(zhí)行。
首先看下BreadFactory類:
1 public class BreadFactory { 2 //生產(chǎn)面包個數(shù)計數(shù)器 3 private int count = 0; 4 //線程的鎖 5 private Object o = new Object(); 6 private boolean flag = false; 7 8 public void product() { 9 synchronized (o) { 10 if (flag) { 11 try { 12 o.wait(); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } 16 } 17 try { 18 Thread.sleep(2000); 19 } catch (InterruptedException e) { 20 e.printStackTrace(); 21 } 22 System.out.println(Thread.currentThread().getName()+"生產(chǎn)了第" + (++count) + "個面包"); 23 flag = true; 24 o.notify(); 25 } 26 } 27 28 public void consume() { 29 synchronized (o) { 30 if (!flag) { 31 try { 32 o.wait(); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 } 37 try { 38 Thread.sleep(2000); 39 } catch (InterruptedException e) { 40 e.printStackTrace(); 41 } 42 System.out.println(Thread.currentThread().getName()+"消費第" + count + "個面包"); 43 flag = false; 44 o.notify(); 45 } 46 } 47 }此類就是負(fù)責(zé)生產(chǎn),消費面包,flag主要用于控制線程之間的切換。
接下來我們看下Producter,Consumer類:
1 public class Producter extends Thread { 2 3 private BreadFactory mBreadFactory; 4 5 public Producter(BreadFactory mBreadFactory) { 6 super(); 7 this.mBreadFactory = mBreadFactory; 8 } 9 10 @Override 11 public void run() { 12 // 13 while (true) { 14 mBreadFactory.product(); 15 } 16 } 17 }很簡單,初始化的時候需要傳遞進(jìn)來一個BreadFactory實例對象,線程啟動的時候調(diào)用BreadFactory類中product()方法不停生產(chǎn)面包。
Consumer類同理:
1 public class Consumer extends Thread { 2 3 private BreadFactory mBreadFactory; 4 5 public Consumer(BreadFactory mBreadFactory) { 6 super(); 7 this.mBreadFactory = mBreadFactory; 8 } 9 10 @Override 11 public void run() { 12 // 13 while (true) { 14 mBreadFactory.consume(); 15 } 16 } 17 }最后看下main方法:
1 public static void main(String[] args) { 2 // 3 BreadFactory factory = new BreadFactory(); 4 Producter p1 = new Producter(factory); 5 p1.start(); 6 Consumer c1 = new Consumer(factory); 7 c1.start(); 8 }沒什么要多說的,就是初始化并啟動線程,運行程序,輸出如下:
Thread-0生產(chǎn)了第1個面包 Thread-1消費第1個面包 Thread-0生產(chǎn)了第2個面包 Thread-1消費第2個面包 Thread-0生產(chǎn)了第3個面包 Thread-1消費第3個面包 Thread-0生產(chǎn)了第4個面包 Thread-1消費第4個面包。。。。。
三、多生產(chǎn)者消費者模式
似乎很順利的就實現(xiàn)了啊,但是實際需求中怎么可能只有一個生產(chǎn)者,一個消費者,生產(chǎn)者,消費者是有多個的,我們試下多個生產(chǎn)者,消費者是什么現(xiàn)象,修改main中邏輯:
1 public static void main(String[] args) { 2 // 3 BreadFactory factory = new BreadFactory(); 4 Producter p1 = new Producter(factory); 5 p1.start(); 6 Consumer c1 = new Consumer(factory); 7 c1.start(); 8 Producter p2 = new Producter(factory); 9 p2.start(); 10 Consumer c2 = new Consumer(factory); 11 c2.start(); 12 }我們就是只多添加了一個生產(chǎn)者和一個消費者,其余沒任何變化。
運行程序,輸出信息如下:
。。。 Thread-2生產(chǎn)了第4個面包 Thread-1消費第4個面包 Thread-2生產(chǎn)了第5個面包 Thread-1消費第5個面包 Thread-2生產(chǎn)了第6個面包 Thread-1消費第6個面包 Thread-3消費第6個面包 Thread-0生產(chǎn)了第7個面包 Thread-3消費第7個面包 。。。咦?生產(chǎn)到第6個面包,竟然被消費了兩次,這顯然是不正常的,那是哪里出問題了呢?
四、多生產(chǎn)者消費者模式問題產(chǎn)生原因分析
接下來,我們直接分析問題產(chǎn)生的原因,我們分析下BreadFactory中product()與consume()方法:
1 public void product() { 2 synchronized (o) { 3 if (flag) { 4 try { 5 o.wait(); 6 } catch (InterruptedException e) { 7 e.printStackTrace(); 8 } 9 } 10 try { 11 Thread.sleep(100); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 System.out.println(Thread.currentThread().getName()+"生產(chǎn)了第" + (++count) + "個面包"); 16 flag = true; 17 o.notify(); 18 } 19 } 20 21 public void consume() { 22 synchronized (o) { 23 if (!flag) { 24 try { 25 o.wait(); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 } 30 try { 31 Thread.sleep(100); 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } 35 System.out.println(Thread.currentThread().getName()+"消費第" + count + "個面包"); 36 flag = false; 37 o.notify(); 38 } 39 }從線程啟動順序以及打印信息可以看出線程0,線程2負(fù)責(zé)生產(chǎn)面包,線程1,線程3負(fù)責(zé)消費面包。
線程執(zhí)行過程中,線程1消費掉第5個面包,此時flag置為false,執(zhí)行notify()方法喚醒其余線程爭取鎖獲取執(zhí)行權(quán)。
此時線程3獲取線程執(zhí)行權(quán),執(zhí)行consume()業(yè)務(wù)邏輯flag此時為false,進(jìn)入if(!flag)邏輯,執(zhí)行wait()方法,此時線程3進(jìn)入wait狀態(tài),停留在25行代碼處。釋放鎖資源,其余線程可以爭取執(zhí)行權(quán)。
此時線程1獲取執(zhí)行權(quán),和線程3一樣,最終停留在25行代碼處。釋放鎖資源,其余線程可以爭取執(zhí)行權(quán)。注意:此時線程1,線程3都停留在25行代碼處,處于wait狀態(tài)。
接下來線程2獲取執(zhí)行權(quán),執(zhí)行生產(chǎn)業(yè)務(wù),生產(chǎn)了第6個面包,然后釋放鎖資源,其余線程可以爭取執(zhí)行權(quán)。
然后線程1又獲取執(zhí)行權(quán),上面說了線程1停留在25行代碼處,現(xiàn)在獲取執(zhí)行權(quán)從25行代碼處開始執(zhí)行,消費掉第6個面包沒問題,flag置為false。然后釋放鎖資源,其余線程可以爭取執(zhí)行權(quán)。
此時線程3又獲取執(zhí)行權(quán),上面分析時說了線程3處于25行代碼處wait狀態(tài),現(xiàn)在獲取執(zhí)行權(quán)從25行代碼處開始執(zhí)行,又消費了第6個面包,到這里面包6被消耗了兩次。
經(jīng)過上面分析已經(jīng)知道產(chǎn)生問題的原因了,線程獲取執(zhí)行權(quán)后直接從wait處開始繼續(xù)執(zhí)行,不在檢查if條件是否成立,這里就是問題產(chǎn)生的原因了。
那怎么修改的呢?很簡單了,將if判斷改為while條件判斷就可以了,這樣線程獲取執(zhí)行權(quán)后還會再次檢查while條件判斷是否成立。
運行程序打印Log如下:
1 。。。 2 Thread-1消費第19個面包 3 Thread-0生產(chǎn)了第20個面包 4 Thread-1消費第20個面包 5 Thread-2生產(chǎn)了第21個面包看輸出Log上面問題是解決了,生產(chǎn)一個面包只會消費一次,但是發(fā)現(xiàn)程序運行自己終止了,上面生產(chǎn)到第21個面包程序似乎不運行了沒Log輸出了,這是什么原因呢?
五、notify()通知丟失問題以及notify()與notifyAll()的區(qū)別
要想明白上述問題產(chǎn)生的原因我們就必須搞懂notify()與notifyAll()的區(qū)別。簡單說就是notify()只會喚醒同一監(jiān)視器處于wait狀態(tài)的一個線程(隨機(jī)喚醒),
而notifyAll()會喚醒同一監(jiān)視器處于wait狀態(tài)的所有線程。
我們分析上面問題產(chǎn)生的原因:線程0,線程2負(fù)責(zé)生產(chǎn)面包,線程1,線程3負(fù)責(zé)消費面包,在程序運行過程存在如下情況:
線程1,3處于consume()中的wait()處,線程0處于product()中wait()處,此時線程2生產(chǎn)完第21個面包執(zhí)行notify()方法,通知處于同一監(jiān)視器下處于wait狀態(tài)線程,此時處于wait狀態(tài)線程為線程1,線程3與線程0,按理說我們是想喚醒一個線程1,3中一個線程來消費剛剛生產(chǎn)的面包,但是程序可不知道啊,調(diào)用notify方法隨機(jī)喚醒一個線程,碰巧此時喚醒的還是生產(chǎn)線程0,這就是notify通知丟失問題,線程0執(zhí)while判斷又處于wait狀態(tài)了,到這里就出現(xiàn)了控制臺沒有Log輸出現(xiàn)象了,經(jīng)過上面分析我們該明白問題出現(xiàn)的原因就是notify通知丟失問題,通知了一個我們不想通知的線程,那怎么解決呢?很簡單了,程序中notify()方法改為notifyAll()就可以了,改為notifyAll()方法上述線程2通知的時候會一起喚醒線程0,1,3,也就是喚醒同一監(jiān)視器處于wait狀態(tài)的所有線程,到這里運行程序就沒有什么問題了。
六、notify()與notifyAll()性能問題
也許有些同學(xué)有疑問了,既然notify()方法會產(chǎn)生問題,那我就用notifyAll()不就完了,直接屏蔽掉notify()方法。這樣做當(dāng)然是很Low的做法。
假設(shè)有N個線程在wait狀態(tài)下,調(diào)用notifyall會喚醒所有線程,然后這N個線程競爭同一個鎖,最后只有一個線程能夠得到鎖,其它線程又回到wait狀態(tài)。這意味每一次喚醒操作可能帶來大量的競爭鎖的請求。這對于頻繁的喚醒操作而言性能上可能是一種災(zāi)難。如果說總是只有一個線程被喚醒后能夠拿到鎖,這種情況下使用notify的性能是要高于notifyall的。
七、JDK1.5中Condition通知機(jī)制
JDK1.5中Condition通知機(jī)制這里就不詳細(xì)講解了,Condition中await(),signal(),signalAll()相當(dāng)于傳統(tǒng)線程通信機(jī)制中wait(),notify(),notifyAll()方法。
我們修改BreadFactory類如下,其余類均不變:
1 public class BreadFactory { 2 // 生產(chǎn)面包個數(shù)計數(shù)器 3 private int count = 0; 4 // 線程的鎖 5 private Lock lock = new ReentrantLock(); 6 private Condition consumeCon = lock.newCondition(); 7 private Condition productCon = lock.newCondition(); 8 private boolean flag = false; 9 10 public void product() { 11 lock.lock(); 12 try { 13 while (flag) { 14 try { 15 productCon.await(); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 } 20 try { 21 Thread.sleep(100); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 System.out.println(Thread.currentThread().getName() + "生產(chǎn)了第" 26 + (++count) + "個面包"); 27 flag = true; 28 consumeCon.signal(); 29 } finally { 30 // 31 lock.unlock(); 32 } 33 } 34 35 public void consume() { 36 lock.lock(); 37 try { 38 while (!flag) { 39 try { 40 consumeCon.await(); 41 } catch (InterruptedException e) { 42 e.printStackTrace(); 43 } 44 } 45 try { 46 Thread.sleep(100); 47 } catch (InterruptedException e) { 48 e.printStackTrace(); 49 } 50 System.out.println(Thread.currentThread().getName() + "消費第" + count 51 + "個面包"); 52 flag = false; 53 productCon.signal(); 54 } finally { 55 // 56 lock.unlock(); 57 } 58 } 59 }其強(qiáng)大之處就在于代碼中6,7,15,28,40,53行代碼處,我們并沒有調(diào)用signalAll()方法,而是調(diào)用的signal()方法。
這樣我們就可以控制在生產(chǎn)完一個面包去喚醒消費的線程來消費面包,而不用連同生產(chǎn)線程一起喚醒,這就是其強(qiáng)大之處,這里就不詳細(xì)分析了,不太熟悉的同學(xué)可自行搜索其余博客學(xué)習(xí)一下,比較簡單,但是很基礎(chǔ)很重要的。
關(guān)于線程間通信問題本篇到此就結(jié)束了,再說一次,多線程相關(guān)博客沒什么新玩意,只是自己工作以來一次總結(jié),雖然基礎(chǔ),枯燥,但是比較重要,希望本篇博客對您有用。
?聲明:文章將會陸續(xù)搬遷到個人公眾號,以后文章也會第一時間發(fā)布到個人公眾號,及時獲取文章內(nèi)容請關(guān)注公眾號
轉(zhuǎn)載于:https://www.cnblogs.com/leipDao/p/8310974.html
總結(jié)
以上是生活随笔為你收集整理的java线程间通信:一个小Demo完全搞懂的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: thinkphp 获取客户端ip地址方法
- 下一篇: 回答自己的提问