經過前面三篇博客的介紹,基本上對Java的多線程有了必定的瞭解了,而後這篇博客根據生產者和消費者的模型來介紹Java多線程的一些其餘知識。html
咱們這裏的生產者和消費者模型爲:java
生產者Producer 生產某個對象(共享資源),放在緩衝池中,而後消費者從緩衝池中取出這個對象。也就是生產者生產一個,消費者取出一個。這樣進行循環。程序員
第一步:咱們先建立共享資源的類 Person,它有兩個方法,一個生產對象,一個消費對象多線程
public class Person { private String name; private int age; /** * 生產數據 * @param name * @param age */ public void push(String name,int age){ this.name = name; this.age = age; } /** * 取數據,消費數據 * @return */ public void pop(){ System.out.println(this.name+"---"+this.age); } }
第二步:建立生產者線程,並在 run() 方法中生產50個對象ide
public class Producer implements Runnable{ //共享資源對象 Person p = null; public Producer(Person p){ this.p = p; } @Override public void run() { //生產對象 for(int i = 0 ; i < 50 ; i++){ //若是是偶數,那麼生產對象 Tom--11;若是是奇數,則生產對象 Marry--21 if(i%2==0){ p.push("Tom", 11); }else{ p.push("Marry", 21); } } } }
第三步:建立消費者線程,並在 run() 方法中消費50個對象this
public class Consumer implements Runnable{ //共享資源對象 Person p = null; public Consumer(Person p) { this.p = p; } @Override public void run() { for(int i = 0 ; i < 50 ; i++){ //消費對象 p.pop(); } } }
因爲咱們的模型是生產一個,立刻消費一個,那指望的結果即是 Tom---11,Marry--21,Tom---11,Mary---21...... 連續這樣交替出現50次線程
可是結果倒是:htm
Marry---21 Marry---21 Marry---21 Marry---21 Marry---21 ...... Marry---21 Marry---21 Marry---21 Marry---21 Marry---21
爲了讓結果產生的更加明顯,咱們在共享資源的 pop() 和 push() 方法中添加一段延時代碼對象
/** * 生產數據 * @param name * @param age */ public void push(String name,int age){ this.name = name; try { //這段延時代碼的做用是可能只生產了 name,age爲nul,消費者就拿去消費了 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } this.age = age; } /** * 取數據,消費數據 * @return */ public void pop(){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.name+"---"+this.age); }
這個時候,結果以下:blog
Marry---11 Tom---21 Marry---11 Tom---21 Marry---11 Tom---21 Marry---11 Tom---21 ...... Tom---11 Tom---21 Marry---11 Tom---21 Marry---11 Marry---21
結果分析:這時候咱們發現結果全亂套了,Marry--21是固定的,Tom--11是固定的,可是上面的結果所有亂了,那這又是爲何呢?並且有不少重複的數據連續出現,那這又是爲何呢?
緣由1:出現錯亂數據,是由於先生產出Tom--11,可是消費者沒有消費,而後生產者繼續生產出name爲Marry,可是age尚未生產,而消費者這個時候拿去消費了,那麼便出現 Marry--11。同理也會出現 Tom--21
緣由2:出現重複數據,是由於生產者生產一份數據了,消費者拿去消費了,可是第二次生產者生產數據了,可是消費者沒有去消費;而第三次生產者繼續生產數據,消費者纔開始消費,這便會產生重複
解決辦法1:生產者生產name和age必需要是一個總體一塊兒完成,即同步。生產的中間不能讓消費者來消費便可。便不會產生錯亂的數據。如何同步能夠參考:
Java 多線程詳解(三)------線程的同步:http://www.cnblogs.com/ysocean/p/6883729.html
這裏咱們選擇同步方法(在方法前面加上 synchronized)
public class Person { private String name; private int age; /** * 生產數據 * @param name * @param age */ public synchronized void push(String name,int age){ this.name = name; try { //這段延時代碼的做用是可能只生產了 name,age爲nul,消費者就拿去消費了 Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } this.age = age; } /** * 取數據,消費數據 * @return */ public synchronized void pop(){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(this.name+"---"+this.age); } }
結果以下:
Marry---21 Marry---21 Marry---21 Marry---21 Marry---21 Tom---11 Tom---11 ...... Tom---11 Tom---11 Tom---11 Tom---11 Tom---11
問題:仍是沒有解決上面的問題2,出現重複的問題。指望的結果是 Tom---11,Marry--21,Tom---11,Mary---21...... 連續這樣交替出現50次。那如何解決呢?
解決辦法:生產者生產一次數據了,就暫停生產者線程,等待消費者消費;消費者消費完了,消費者線程暫停,等待生產者生產數據,這樣來進行。
這裏咱們介紹一個同步鎖池的概念:
同步鎖池:同步鎖必須選擇多個線程共同的資源對象,而一個線程得到鎖的時候,別的線程都在同步鎖池等待獲取鎖;當那個線程釋放同步鎖了,其餘線程便開始由CPU調度分配鎖
關於讓線程等待和喚醒線程的方法,以下:(這是 Object 類中的方法)
wait():執行該方法的線程對象,釋放同步鎖,JVM會把該線程放到等待池中,等待其餘線程喚醒該線程
notify():執行該方法的線程喚醒在等待池中等待的任意一個線程,把線程轉到鎖池中等待(注意鎖池和等待池的區別)
notifyAll():執行該方法的線程喚醒在等待池中等待的全部線程,把線程轉到鎖池中等待。
注意:上述方法只能被同步監聽鎖對象來調用,這也是爲啥wait() 和 notify()方法都在 Object 對象中,由於同步監聽鎖能夠是任意對象,只不過必須是須要同步線程的共同對象便可,不然別的對象調用會報錯: java.lang.IllegalMonitorStateException
假設 A 線程和 B 線程同時操做一個 X 對象,A,B 線程能夠經過 X 對象的 wait() 和 notify() 方法來進行通訊,流程以下:
①、當線程 A 執行 X 對象的同步方法時,A 線程持有 X 對象的 鎖,B線程在 X 對象的鎖池中等待
②、A線程在同步方法中執行 X.wait() 方法時,A線程釋放 X 對象的鎖,進入 X 對象的等待池中
③、在 X 對象的鎖池中等待鎖的 B 線程得到 X 對象的鎖,執行 X 的另外一個同步方法
④、B 線程在同步方法中執行 X.notify() 方法,JVM 把 A 線程從等待池中移動到 X 對象的鎖池中,等待獲取鎖
⑤、B 線程執行完同步方法,釋放鎖,等待獲取鎖的 A 線程得到鎖,繼續執行同步方法
那麼爲了解決上面重複的問題,修改代碼以下:
public class Person { private String name; private int age; //表示共享資源對象是否爲空,若是爲 true,表示須要生產,若是爲 false,則有數據了,不要生產 private boolean isEmpty = true; /** * 生產數據 * @param name * @param age */ public synchronized void push(String name,int age){ try { //不能用 if,由於可能有多個線程 while(!isEmpty){//進入到while語句內,說明 isEmpty==false,那麼表示有數據了,不能生產,必需要等待消費者消費 this.wait();//致使當前線程等待,進入等待池中,只能被其餘線程喚醒 } //-------生產數據開始------- this.name = name; //延時代碼 Thread.sleep(10); this.age = age; //-------生產數據結束------- isEmpty = false;//設置 isEmpty 爲 false,表示已經有數據了 this.notifyAll();//生產完畢,喚醒全部消費者 } catch (Exception e) { e.printStackTrace(); } } /** * 取數據,消費數據 * @return */ public synchronized void pop(){ try { //不能用 if,由於可能有多個線程 while(isEmpty){//進入 while 代碼塊,表示 isEmpty==true,表示爲空,等待生產者生產數據,消費者要進入等待池中 this.wait();//消費者線程等待 } //-------消費開始------- Thread.sleep(10); System.out.println(this.name+"---"+this.age); //-------消費結束------ isEmpty = true;//設置 isEmpty爲true,表示須要生產者生產對象 this.notifyAll();//消費完畢,喚醒全部生產者 } catch (InterruptedException e) { e.printStackTrace(); } } }
結果:
Tom---11 Marry---21 Tom---11 Marry---21 Tom---11 Marry---21 Tom---11 ...... Marry---21 Tom---11 Marry---21 Tom---11 Marry---21 Tom---11 Marry---21
那麼這即是咱們期待的結果,交替出現。
死鎖:
①、多線程通訊的時候,很容易形成死鎖,死鎖沒法解決,只能避免
②、當 A 線程等待由 B 線程持有的鎖,而 B 線程正在等待由 A 線程持有的鎖時發生死鎖現象(好比A拿着鉛筆,B拿着圓珠筆,A說你先給我圓珠筆,我就把鉛筆給你,而B說你先給我鉛筆,我就把圓珠筆給你,這就形成了死鎖,A和B永遠不能進行交換)
③、JVM 既不檢測也不避免這種現象,因此程序員必須保證不能出現這樣的狀況
Thread 類中容易形成死鎖的方法(這兩個方法都已通過時了,不建議使用):
suspend():使正在運行的線程放棄 CPU,暫停運行(不釋放鎖)
resume():使暫停的線程恢復運行
情景:A 線程得到對象鎖,正在執行一個同步方法,若是 B線程調用 A 線程的 suspend() 方法,此時A 暫停運行,放棄 CPU 資源,可是不放棄同步鎖,那麼B也不能得到鎖,A又暫停,那麼便形成死鎖。
解決死鎖法則:當多個線程須要訪問 共同的資源A,B,C時,必須保證每個線程按照必定的順序去訪問,好比都先訪問A,而後B,最後C。就像咱們這裏的生產者---消費者模型,制定了必須生產者先生產一個對象,而後消費者去消費,消費完畢,生產者才能在開始生產,而後消費者在消費。這樣的順序便不會形成死鎖。