版權聲明:本文出自汪磊的博客,轉載請務必注明出處。
Java線程系列文章只是自己知識的總結梳理,都是最基礎的玩意,已經掌握熟練的可以繞過。
一、從一個小Demo說起
上篇我們聊到了Java多線程的同步機制:Java多線程同步問題:一個小Demo完全搞懂。這篇我們聊一下java多線程之間的通信機制。
上一篇探討java同步機制的時候我們舉得例子輸出log現象是:一段時間總是A線程輸出而另一段時間總是B線程輸出,有沒有一種方式可以控制A,B線程交錯輸出呢?答案是當然可以了,這時候我們就要用到多線程的wait/notify機制了。
wait/notify機制就是當線程A執行到某一對象的wait()方法時,就會進入等待狀態,此時線程A放棄持有的鎖,其余線程可以競爭鎖的持有權。當有其余線程調用notify()或者notifyAll()方法的時候就可能(當有多個線程的時候notify()方法只會喚醒處於等待狀態線程中的一個)喚醒線程A,使其從wait狀態醒來,繼續向下執行業務邏輯。
接下來,我們通過一個小demo加以理解。
二、單生產者消費者模式
demo很簡單,就是開啟兩個線程,一個生產面包,另一個負責消費面包,並且生產一個就要消費一個,交替執行。
首先看下BreadFactory類:
1 public class BreadFactory { 2 //生產面包個數計數器 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()+"生產了第" + (++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 }
此類就是負責生產,消費面包,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 }
很簡單,初始化的時候需要傳遞進來一個BreadFactory實例對象,線程啟動的時候調用BreadFactory類中product()方法不停生產面包。
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生產了第1個面包 Thread-1消費第1個面包 Thread-0生產了第2個面包 Thread-1消費第2個面包 Thread-0生產了第3個面包 Thread-1消費第3個面包 Thread-0生產了第4個面包 Thread-1消費第4個面包
。。。。。
三、多生產者消費者模式
似乎很順利的就實現了啊,但是實際需求中怎么可能只有一個生產者,一個消費者,生產者,消費者是有多個的,我們試下多個生產者,消費者是什么現象,修改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 }
我們就是只多添加了一個生產者和一個消費者,其余沒任何變化。
運行程序,輸出信息如下:
。。。 Thread-2生產了第4個面包 Thread-1消費第4個面包 Thread-2生產了第5個面包 Thread-1消費第5個面包 Thread-2生產了第6個面包 Thread-1消費第6個面包 Thread-3消費第6個面包 Thread-0生產了第7個面包 Thread-3消費第7個面包 。。。
咦?生產到第6個面包,竟然被消費了兩次,這顯然是不正常的,那是哪里出問題了呢?
四、多生產者消費者模式問題產生原因分析
接下來,我們直接分析問題產生的原因,我們分析下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()+"生產了第" + (++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負責生產面包,線程1,線程3負責消費面包。
線程執行過程中,線程1消費掉第5個面包,此時flag置為false,執行notify()方法喚醒其余線程爭取鎖獲取執行權。
此時線程3獲取線程執行權,執行consume()業務邏輯flag此時為false,進入if(!flag)邏輯,執行wait()方法,此時線程3進入wait狀態,停留在25行代碼處。釋放鎖資源,其余線程可以爭取執行權。
此時線程1獲取執行權,和線程3一樣,最終停留在25行代碼處。釋放鎖資源,其余線程可以爭取執行權。注意:此時線程1,線程3都停留在25行代碼處,處於wait狀態。
接下來線程2獲取執行權,執行生產業務,生產了第6個面包,然后釋放鎖資源,其余線程可以爭取執行權。
然后線程1又獲取執行權,上面說了線程1停留在25行代碼處,現在獲取執行權從25行代碼處開始執行,消費掉第6個面包沒問題,flag置為false。然后釋放鎖資源,其余線程可以爭取執行權。
此時線程3又獲取執行權,上面分析時說了線程3處於25行代碼處wait狀態,現在獲取執行權從25行代碼處開始執行,又消費了第6個面包,到這里面包6被消耗了兩次。
經過上面分析已經知道產生問題的原因了,線程獲取執行權后直接從wait處開始繼續執行,不在檢查if條件是否成立,這里就是問題產生的原因了。
那怎么修改的呢?很簡單了,將if判斷改為while條件判斷就可以了,這樣線程獲取執行權后還會再次檢查while條件判斷是否成立。
運行程序打印Log如下:
1 。。。 2 Thread-1消費第19個面包 3 Thread-0生產了第20個面包 4 Thread-1消費第20個面包 5 Thread-2生產了第21個面包
看輸出Log上面問題是解決了,生產一個面包只會消費一次,但是發現程序運行自己終止了,上面生產到第21個面包程序似乎不運行了沒Log輸出了,這是什么原因呢?
五、notify()通知丟失問題以及notify()與notifyAll()的區別
要想明白上述問題產生的原因我們就必須搞懂notify()與notifyAll()的區別。簡單說就是notify()只會喚醒同一監視器處於wait狀態的一個線程(隨機喚醒),
而notifyAll()會喚醒同一監視器處於wait狀態的所有線程。
我們分析上面問題產生的原因:線程0,線程2負責生產面包,線程1,線程3負責消費面包,在程序運行過程存在如下情況:
線程1,3處於consume()中的wait()處,線程0處於product()中wait()處,此時線程2生產完第21個面包執行notify()方法,通知處於同一監視器下處於wait狀態線程,此時處於wait狀態線程為線程1,線程3與線程0,按理說我們是想喚醒一個線程1,3中一個線程來消費剛剛生產的面包,但是程序可不知道啊,調用notify方法隨機喚醒一個線程,碰巧此時喚醒的還是生產線程0,這就是notify通知丟失問題,線程0執while判斷又處於wait狀態了,到這里就出現了控制台沒有Log輸出現象了,經過上面分析我們該明白問題出現的原因就是notify通知丟失問題,通知了一個我們不想通知的線程,那怎么解決呢?很簡單了,程序中notify()方法改為notifyAll()就可以了,改為notifyAll()方法上述線程2通知的時候會一起喚醒線程0,1,3,也就是喚醒同一監視器處於wait狀態的所有線程,到這里運行程序就沒有什么問題了。
六、notify()與notifyAll()性能問題
也許有些同學有疑問了,既然notify()方法會產生問題,那我就用notifyAll()不就完了,直接屏蔽掉notify()方法。這樣做當然是很Low的做法。
假設有N個線程在wait狀態下,調用notifyall會喚醒所有線程,然后這N個線程競爭同一個鎖,最后只有一個線程能夠得到鎖,其它線程又回到wait狀態。這意味每一次喚醒操作可能帶來大量的競爭鎖的請求。這對於頻繁的喚醒操作而言性能上可能是一種災難。如果說總是只有一個線程被喚醒后能夠拿到鎖,這種情況下使用notify的性能是要高於notifyall的。
七、JDK1.5中Condition通知機制
JDK1.5中Condition通知機制這里就不詳細講解了,Condition中await(),signal(),signalAll()相當於傳統線程通信機制中wait(),notify(),notifyAll()方法。
我們修改BreadFactory類如下,其余類均不變:
1 public class BreadFactory { 2 // 生產面包個數計數器 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() + "生產了第" 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 }
其強大之處就在於代碼中6,7,15,28,40,53行代碼處,我們並沒有調用signalAll()方法,而是調用的signal()方法。
這樣我們就可以控制在生產完一個面包去喚醒消費的線程來消費面包,而不用連同生產線程一起喚醒,這就是其強大之處,這里就不詳細分析了,不太熟悉的同學可自行搜索其余博客學習一下,比較簡單,但是很基礎很重要的。
關於線程間通信問題本篇到此就結束了,再說一次,多線程相關博客沒什么新玩意,只是自己工作以來一次總結,雖然基礎,枯燥,但是比較重要,希望本篇博客對您有用。
聲明:文章將會陸續搬遷到個人公眾號,以后文章也會第一時間發布到個人公眾號,及時獲取文章內容請關注公眾號