日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

java多線程同步以及線程間通信詳解&消費者生產者模式&死鎖&Thread.join()(多線程編程之二)

 沙門空海 2018-01-16
多線程系列教程:
java多線程-概念&創(chuàng)建啟動&中斷&守護線程&優(yōu)先級&線程狀態(tài)(多線程編程之一)
java多線程同步以及線程間通信詳解&消費者生產者模式&死鎖&Thread.join()(多線程編程之二)
java&android線程池-Executor框架之ThreadPoolExcutor&ScheduledThreadPoolExecutor淺析(多線程編程之三)
Java多線程:Callable、Future和FutureTask淺析(多線程編程之四)

本篇我們將討論以下知識點:

1.線程同步問題的產生

什么是線程同步問題,我們先來看一段賣票系統(tǒng)的代碼,然后再分析這個問題:
  1. package com.zejian.test;  
  2. /** 
  3.  * @author zejian 
  4.  * @time 2016年3月12日 下午2:55:42 
  5.  * @decrition 模擬賣票線程 
  6.  */  
  7. public class Ticket implements Runnable  
  8. {  
  9.     //當前擁有的票數(shù)  
  10.     private  int num = 100;  
  11.     public void run()  
  12.     {  
  13.         while(true)  
  14.         {  
  15.                 if(num>0)  
  16.                 {  
  17.                     try{Thread.sleep(10);}catch (InterruptedException e){}  
  18.                     //輸出賣票信息  
  19.                     System.out.println(Thread.currentThread().getName()+".....sale...."+num--);  
  20.                 }  
  21.         }  
  22.     }  
  23. }  
上面是賣票線程類,下來再來看看執(zhí)行類:
  1. package com.zejian.test;  
  2. /**  
  3.  * @author zejian  
  4.  * @time 2016年3月12日 下午2:54:18  
  5.  * @decrition 模擬賣票系統(tǒng),該案例只考慮單方面賣票,其他情況暫時不考慮  
  6.  */  
  7. public class TicketDemo {  
  8.       
  9.     public static void main(String[] args)   
  10.     {  
  11.         Ticket t = new Ticket();//創(chuàng)建一個線程任務對象。  
  12.           
  13.         //創(chuàng)建4個線程同時賣票  
  14.         Thread t1 = new Thread(t);  
  15.         Thread t2 = new Thread(t);  
  16.         Thread t3 = new Thread(t);  
  17.         Thread t4 = new Thread(t);  
  18.         //啟動線程  
  19.         t1.start();  
  20.         t2.start();  
  21.         t3.start();  
  22.         t4.start();  
  23.     }  
  24. }  
運行程序結果如下(僅截取部分數(shù)據(jù)):


從運行結果,我們就可以看出我們4個售票窗口同時賣出了1號票,這顯然是不合邏輯的,其實這個問題就是我們前面所說的線程同步問題。不同的線程都對同一個數(shù)據(jù)進了操作這就容易導致數(shù)據(jù)錯亂的問題,也就是線程不同步。那么這個問題該怎么解決呢?在給出解決思路之前我們先來分析一下這個問題是怎么產生的?我們聲明一個線程類Ticket,在這個類中我們又聲明了一個成員變量num也就是票的數(shù)量,然后我們通過run方法不斷的去獲取票數(shù)并輸出,最后我們在外部類TicketDemo中創(chuàng)建了四個線程同時操作這個數(shù)據(jù),運行后就出現(xiàn)我們剛才所說的線程同步問題,從這里我們可以看出產生線程同步(線程安全)問題的條件有兩個:1.多個線程在操作共享的數(shù)據(jù)(num),2.操作共享數(shù)據(jù)的線程代碼有多條(4條線程);既然原因知道了,那該怎么解決?
解決思路:將多條操作共享數(shù)據(jù)的線程代碼封裝起來,當有線程在執(zhí)行這些代碼的時候,其他線程時不可以參與運算的。必須要當前線程把這些代碼都執(zhí)行完畢后,其他線程才可以參與運算。 好了,思路知道了,我們就用java代碼的方式來解決這個問題。
2.解決線程同步的兩種典型方案
在java中有兩種機制可以防止線程安全的發(fā)生,Java語言提供了一個synchronized關鍵字來解決這問題,同時在Java SE5.0引入了Lock鎖對象的相關類,接下來我們分別介紹這兩種方法
2.1通過鎖(Lock)對象的方式解決線程安全問題
在給出解決代碼前我們先來介紹一個知識點:Lock,鎖對象。在java中鎖是用來控制多個線程訪問共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源(但有的鎖可以允許多個線程并發(fā)訪問共享資源,比如讀寫鎖,后面我們會分析)。在Lock接口出現(xiàn)之前,java程序是靠synchronized關鍵字(后面分析)實現(xiàn)鎖功能的,而JAVA SE5.0之后并發(fā)包中新增了Lock接口用來實現(xiàn)鎖的功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖,缺點就是缺少像synchronized那樣隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性,可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。接下來我們就來介紹Lock接口的主要API方便我們學習
方法相關描述內容                                                                                                                                                                                                                                                                                                                  
void lock()獲取鎖,調用該方法當前線程會獲取鎖,當獲取鎖后。從該方法返回
void lockInterruptibly()
throws InterruptedException
可中斷獲取鎖和lock()方法不同的是該方法會響應中斷,即在獲取鎖
中可以中斷當前線程。例如某個線程在等待一個鎖的控制權的這段時
間需要中斷。
boolean tryLock()嘗試非阻塞獲取鎖,調用該方法后立即返回,如果能夠獲取鎖則返回
true,否則返回false。
boolean tryLock(long time,TimeUnit unit)
throws  InterruptedException
超時獲取鎖,當前線程在以下3種情況返回:
1.當前線程在超時時間內獲取了鎖
2.當前線程在超時時間被中斷
3.當前線程超時時間結束,返回false
void unlock()釋放鎖
Condition newCondition()條件對象,獲取等待通知組件。該組件和當前的鎖綁定,當前線程只有
獲取了鎖,才能調用該組件的await()方法,而調用后,當前線程將縮放
鎖。
這里先介紹一下API,后面我們將結合Lock接口的實現(xiàn)子類ReentrantLock使用某些方法。
ReentrantLock(重入鎖):
重入鎖,顧名思義就是支持重新進入的鎖,它表示該鎖能夠支持一個線程對資源的重復加鎖,也就是說在調用lock()方法時,已經獲取到鎖的線程,能夠再次調用lock()方法獲取鎖而不被阻塞,同時還支持獲取鎖的公平性和非公平性。這里的公平是在絕對時間上,先對鎖進行獲取的請求一定先被滿足,那么這個鎖是公平鎖,反之,是不公平的。那么該如何使用呢?看范例代碼:
1.同步執(zhí)行的代碼跟synchronized類似功能:
  1. ReentrantLock lock = new ReentrantLock(); //參數(shù)默認false,不公平鎖    
  2. ReentrantLock lock = new ReentrantLock(true); //公平鎖    
  3.     
  4. lock.lock(); //如果被其它資源鎖定,會在此等待鎖釋放,達到暫停的效果    
  5. try {    
  6.     //操作    
  7. } finally {    
  8.     lock.unlock();  //釋放鎖  
  9. }    
2.防止重復執(zhí)行代碼:
  1. ReentrantLock lock = new ReentrantLock();    
  2. if (lock.tryLock()) {  //如果已經被lock,則立即返回false不會等待,達到忽略操作的效果     
  3.     try {    
  4.         //操作    
  5.     } finally {    
  6.         lock.unlock();    
  7.    }    
  8. }    
3.嘗試等待執(zhí)行的代碼:
  1. ReentrantLock lock = new ReentrantLock(true); //公平鎖    
  2. try {    
  3.     if (lock.tryLock(5, TimeUnit.SECONDS)) {        
  4.         //如果已經被lock,嘗試等待5s,看是否可以獲得鎖,如果5s后仍然無法獲得鎖則返回false繼續(xù)執(zhí)行    
  5.        try {    
  6.             //操作    
  7.         } finally {    
  8.             lock.unlock();    
  9.         }    
  10.     }    
  11. } catch (InterruptedException e) {    
  12.     e.printStackTrace(); //當前線程被中斷時(interrupt),會拋InterruptedException                     
  13. }    
這里有點需要特別注意的,把解鎖操作放在finally代碼塊內這個十分重要。如果在臨界區(qū)的代碼拋出異常,鎖必須被釋放。否則,其他線程將永遠阻塞。好了,ReentrantLock我們就簡單介紹到這里,接下來我們通過ReentrantLock來解決前面賣票線程的線程同步(安全)問題,代碼如下:
  1. package com.zejian.test;  
  2. import java.util.concurrent.locks.Lock;  
  3. import java.util.concurrent.locks.ReentrantLock;  
  4. /** 
  5.  * @author zejian 
  6.  * @time 2016年3月12日 下午2:55:42 
  7.  * @decrition 模擬賣票線程 
  8.  */  
  9. public class Ticket implements Runnable  
  10. {  
  11.     //創(chuàng)建鎖對象  
  12.     private Lock ticketLock = new ReentrantLock();  
  13.     //當前擁有的票數(shù)  
  14.     private  int num = 100;  
  15.     public void run()  
  16.     {  
  17.         while(true)  
  18.         {         
  19.                 ticketLock.lock();//獲取鎖  
  20.                 if(num>0)  
  21.                 {  
  22.                   
  23.                     try{  
  24.                         Thread.sleep(10);  
  25.                         //輸出賣票信息  
  26.                         System.out.println(Thread.currentThread().getName()+".....sale...."+num--);  
  27.                     }catch (InterruptedException e){  
  28.                         Thread.currentThread().interrupt();//出現(xiàn)異常就中斷  
  29.                     }finally{  
  30.                         ticketLock.unlock();//釋放鎖  
  31.                     }     
  32.                 }  
  33.         }  
  34.     }  
  35. }  
TicketDemo類無需變化,運行結果正常(太多不貼了),線程安全問題就此解決。
2.2通過synchronied關鍵字的方式解決線程安全問題
在Java中內置了語言級的同步原語-synchronized,這個可以大大簡化了Java中多線程同步的使用。從JAVA SE1.0開始,java中的每一個對象都有一個內部鎖,如果一個方法使用synchronized關鍵字進行聲明,那么這個對象將保護整個方法,也就是說調用該方法線程必須獲得內部的對象鎖。
  1. public synchronized void method{  
  2.   //method body  
  3. }  
等價于
  1. private Lock ticketLock = new ReentrantLock();  
  2. public void method{  
  3.  ticketLock.lock();  
  4.  try{  
  5.   //.......  
  6.  }finally{  
  7.    ticketLock.unlock();  
  8.  }  
  9. }  
從這里可以看出使用synchronized關鍵字來編寫代碼要簡潔得多了。當然,要理解這一代碼,我們必須知道每個對象有一個內部鎖,并且該鎖有一個內部條件。由鎖來管理那些試圖進入synchronized方法的線程,由條件來管那些調用wait的線程(wait()/notifyAll/notify())。同時我們必須明白一旦有一個線程通過synchronied方法獲取到內部鎖,該類的所有synchronied方法或者代碼塊都無法被其他線程訪問直到當前線程釋放了內部鎖。剛才上面說的是同步方法,synchronized還有一種同步代碼塊的實現(xiàn)方式:
  1. Object obj = new Object();  
  2. synchronized(obj){  
  3.   //需要同步的代碼  
  4. }  
其中obj是對象鎖,可以是任意對象。那么我們就通過其中的一個方法來解決售票系統(tǒng)的線程同步問題:
  1. class Ticket implements Runnable  
  2. {  
  3.     private  int num = 100;  
  4.     Object obj = new Object();  
  5.     public void run()  
  6.     {  
  7.         while(true)  
  8.         {  
  9.             synchronized(obj)  
  10.             {  
  11.                 if(num>0)  
  12.                 {  
  13.                     try{Thread.sleep(10);}catch (InterruptedException e){}  
  14.                       
  15.                     System.out.println(Thread.currentThread().getName()+".....sale...."+num--);  
  16.                 }  
  17.             }  
  18.         }  
  19.     }  
  20. }  
嗯,同步代碼塊解決,運行結果也正常。到此同步問題也就解決了,當然代碼同步也是要犧牲效率為前提的:
同步的好處:解決了線程的安全問題。
同步的弊端:相對降低了效率,因為同步外的線程的都會判斷同步鎖。
同步的前提:同步中必須有多個線程并使用同一個鎖。
3.線程間的通信機制
線程開始運行,擁有自己的棧空間,但是如果每個運行中的線程,如果僅僅是孤立地運行,那么沒有一點兒價值,或者是價值很小,如果多線程能夠相互配合完成工作的話,這將帶來巨大的價值,這也就是線程間的通信啦。在java中多線程間的通信使用的是等待/通知機制來實現(xiàn)的。
3.1synchronied關鍵字等待/通知機制:是指一個線程A調用了對象O的wait()方法進入等待狀態(tài),而另一個線程B調用了對象O的notify()或者notifyAll()方法,線程A收到通知后從對象O的wait()方法返回,進而執(zhí)行后續(xù)操作。上述的兩個線程通過對象O來完成交互,而對象上的wait()和notify()/notifyAll()的關系就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。
等待/通知機制主要是用到的函數(shù)方法是notify()/notifyAll(),wait()/wait(long),wait(long,int),這些方法在上一篇文章都有說明過,這里就不重復了。當然這是針對synchronied關鍵字修飾的函數(shù)或代碼塊,因為要使用notify()/notifyAll(),wait()/wait(long),wait(long,int)這些方法的前提是對調用對象加鎖,也就是說只能在同步函數(shù)或者同步代碼塊中使用。
3.2條件對象的等待/通知機制:所謂的條件對象也就是配合前面我們分析的Lock鎖對象,通過鎖對象的條件對象來實現(xiàn)等待/通知機制。那么條件對象是怎么創(chuàng)建的呢?
  1. //創(chuàng)建條件對象  
  2. Condition conditionObj=ticketLock.newCondition();  
就這樣我們創(chuàng)建了一個條件對象。注意這里返回的對象是與該鎖(ticketLock)相關的條件對象。下面是條件對象的API:
方法函數(shù)方法對應的描述                                                                                                                                                                                    
void await()將該線程放到條件等待池中(對應wait()方法)
void signalAll()解除該條件等待池中所有線程的阻塞狀態(tài)(對應notifyAll()方法)
void signal()從該條件的等待池中隨機地選擇一個線程,解除其阻塞狀態(tài)(對應notify()方法)
上述方法的過程分析:一個線程A調用了條件對象的await()方法進入等待狀態(tài),而另一個線程B調用了條件對象的signal()或者signalAll()方法,線程A收到通知后從條件對象的await()方法返回,進而執(zhí)行后續(xù)操作。上述的兩個線程通過條件對象來完成交互,而對象上的await()和signal()/signalAll()的關系就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。當然這樣的操作都是必須基于對象鎖的,當前線程只有獲取了鎖,才能調用該條件對象的await()方法,而調用后,當前線程將縮放鎖。
這里有點要特別注意的是,上述兩種等待/通知機制中,無論是調用了signal()/signalAll()方法還是調用了notify()/notifyAll()方法并不會立即激活一個等待線程。它們僅僅都只是解除等待線程的阻塞狀態(tài),以便這些線程可以在當前線程解鎖或者退出同步方法后,通過爭奪CPU執(zhí)行權實現(xiàn)對對象的訪問。到此,線程通信機制的概念分析完,我們下面通過生產者消費者模式來實現(xiàn)等待/通知機制。
4.生產者消費者模式
4.1單生產者單消費者模式
顧名思義,就是一個線程消費,一個線程生產。我們先來看看等待/通知機制下的生產者消費者模式:我們假設這樣一個場景,我們是賣北京烤鴨店鋪,我們現(xiàn)在只有一條生產線也只有一條消費線,也就是說只能生產線程生產完了,再通知消費線程才能去賣,如果消費線程沒烤鴨了,就必須通知生產線程去生產,此時消費線程進入等待狀態(tài)。在這樣的場景下,我們不僅要保證共享數(shù)據(jù)(烤鴨數(shù)量)的線程安全,而且還要保證烤鴨數(shù)量在消費之前必須有烤鴨。下面我們通過java代碼來實現(xiàn):
北京烤鴨生產資源類KaoYaResource:
  1. package com.zejian.test;  
  2. /** 
  3.  * @author zejian 
  4.  * @time 2016年3月12日 下午10:44:25 
  5.  * @decrition 烤鴨資源 
  6.  */  
  7. public class KaoYaResource {  
  8.       
  9.     private String name;  
  10.     private int count = 1;//烤鴨的初始數(shù)量  
  11.     private boolean flag = false;//判斷是否有需要線程等待的標志  
  12.       
  13.     /** 
  14.      * 生產烤鴨 
  15.      */  
  16.     public synchronized void product(String name){  
  17.         if(flag){  
  18.             //此時有烤鴨,等待  
  19.             try {  
  20.                 this.wait();  
  21.             } catch (InterruptedException e) {  
  22.                 e.printStackTrace()  
  23. ;  
  24.             }  
  25.         }  
  26.         this.name=name+count;//設置烤鴨的名稱  
  27.         count++;  
  28.         System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
  29.         flag=true;//有烤鴨后改變標志  
  30.         notifyAll();//通知消費線程可以消費了  
  31.     }  
  32.       
  33.     /** 
  34.      * 消費烤鴨 
  35.      */  
  36.     public synchronized void consume(){  
  37.         if(!flag){//如果沒有烤鴨就等待  
  38.             try{this.wait();}catch(InterruptedException e){}  
  39.         }  
  40.         System.out.println(Thread.currentThread().getName()+"...消費者........"+this.name);//消費烤鴨1  
  41.         flag = false;  
  42.         notifyAll();//通知生產者生產烤鴨  
  43.     }  
  44. }  
在這個類中我們有兩個synchronized的同步方法,一個是生產烤鴨的,一個是消費烤鴨的,之所以需要同步是因為我們操作了共享數(shù)據(jù)count,同時為了保證生產烤鴨后才能消費也就是生產一只烤鴨后才能消費一只烤鴨,我們使用了等待/通知機制,wait()和notify()。當?shù)谝淮芜\行生產現(xiàn)場時調用生產的方法,此時有一只烤鴨,即flag=false,無需等待,因此我們設置可消費的烤鴨名稱然后改變flag=true,同時通知消費線程可以消費烤鴨了,即使此時生產線程再次搶到執(zhí)行權,因為flag=true,所以生產線程會進入等待阻塞狀態(tài),消費線程被喚醒后就進入消費方法,消費完成后,又改變標志flag=false,通知生產線程可以生產烤鴨了.........以此循環(huán)。
生產消費執(zhí)行類Single_Producer_Consumer.java:
  1. package com.zejian.test;  
  2. /** 
  3.  * @author zejian 
  4.  * @time 2016年3月12日 下午10:29:12 
  5.  * @decrition 單生產者單消費者模式 
  6.  */  
  7. public class Single_Producer_Consumer {  
  8.       
  9.     public static void main(String[] args)   
  10.     {  
  11.         KaoYaResource r = new KaoYaResource();  
  12.         Producer pro = new Producer(r);  
  13.         Consumer con = new Consumer(r);  
  14.         //生產者線程  
  15.         Thread t0 = new Thread(pro);  
  16.         //消費者線程  
  17.         Thread t2 = new Thread(con);  
  18.         //啟動線程  
  19.         t0.start();  
  20.         t2.start();  
  21.     }  
  22. }  
  23. /** 
  24.  * @author zejian 
  25.  * @time 2016年3月12日 下午11:02:22 
  26.  * @decrition 生產者線程 
  27.  */  
  28. class Producer implements Runnable  
  29. {  
  30.     private KaoYaResource r;  
  31.     Producer(KaoYaResource r)  
  32.     {  
  33.         this.r = r;  
  34.     }  
  35.     public void run()  
  36.     {  
  37.         while(true)  
  38.         {  
  39.             r.product("北京烤鴨");  
  40.         }  
  41.     }  
  42. }  
  43. /** 
  44.  * @author zejian 
  45.  * @time 2016年3月12日 下午11:02:05 
  46.  * @decrition 消費者線程 
  47.  */  
  48. class Consumer implements Runnable  
  49. {  
  50.     private KaoYaResource r;  
  51.     Consumer(KaoYaResource r)  
  52.     {  
  53.         this.r = r;  
  54.     }  
  55.     public void run()  
  56.     {  
  57.         while(true)  
  58.         {  
  59.             r.consume();  
  60.         }  
  61.     }  
  62. }  
在這個類中我們創(chuàng)建兩個線程,一個是消費者線程,一個是生產者線程,我們分別開啟這兩個線程用于不斷的生產消費,運行結果如下:

很顯然的情況就是生產一只烤鴨然后就消費一只烤鴨。運行情況完全正常,嗯,這就是單生產者單消費者模式。上面使用的是synchronized關鍵字的方式實現(xiàn)的,那么接下來我們使用對象鎖的方式實現(xiàn):KaoYaResourceByLock.java
  1. package com.zejian.test;  
  2. import java.util.concurrent.locks.Condition;  
  3. import java.util.concurrent.locks.Lock;  
  4. import java.util.concurrent.locks.ReentrantLock;  
  5. /** 
  6.  * @author zejian 
  7.  * @time 2016年3月13日 上午9:55:35 
  8.  * @decrition 通過對象鎖的方式來實現(xiàn)等待/通知機制 
  9.  */  
  10. public class KaoyaResourceByLock {  
  11.       
  12.     private String name;  
  13.     private int count = 1;//烤鴨的初始數(shù)量  
  14.     private boolean flag = false;//判斷是否有需要線程等待的標志  
  15.     //創(chuàng)建一個鎖對象  
  16.     private Lock resourceLock=new ReentrantLock();  
  17.     //創(chuàng)建條件對象  
  18.     private Condition condition= resourceLock.newCondition();  
  19.     /** 
  20.      * 生產烤鴨 
  21.      */  
  22.     public  void product(String name){  
  23.         resourceLock.lock();//先獲取鎖  
  24.         try{  
  25.             if(flag){  
  26.                 try {  
  27.                     condition.await();  
  28.                 } catch (InterruptedException e) {  
  29.                     e.printStackTrace();  
  30.                 }  
  31.             }  
  32.             this.name=name+count;//設置烤鴨的名稱  
  33.             count++;  
  34.             System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
  35.             flag=true;//有烤鴨后改變標志  
  36.             condition.signalAll();//通知消費線程可以消費了  
  37.         }finally{  
  38.             resourceLock.unlock();  
  39.         }     
  40.     }  
  41.       
  42.     /** 
  43.      * 消費烤鴨 
  44.      */  
  45.     public  void consume(){  
  46.         resourceLock.lock();  
  47.         try{  
  48.         if(!flag){//如果沒有烤鴨就等待  
  49.             try{condition.await();}catch(InterruptedException e){}  
  50.         }  
  51.         System.out.println(Thread.currentThread().getName()+"...消費者........"+this.name);//消費烤鴨1  
  52.         flag = false;  
  53.         condition.signalAll();//通知生產者生產烤鴨  
  54.         }finally{  
  55.             resourceLock.unlock();  
  56.         }  
  57.     }  
  58. }  
代碼變化不大,我們通過對象鎖的方式去實現(xiàn),首先要創(chuàng)建一個對象鎖,我們這里使用的重入鎖ReestrantLock類,然后通過手動設置lock()和unlock()的方式去獲取鎖以及釋放鎖。為了實現(xiàn)等待/通知機制,我們還必須通過鎖對象去創(chuàng)建一個條件對象Condition,然后通過鎖對象的await()和signalAll()方法去實現(xiàn)等待以及通知操作。Single_Producer_Consumer.java代碼替換一下資源類即可,運行結果就不貼了,有興趣自行操作即可。
4.2多生產者多消費者模式
分析完了單生產者單消費者模式,我們再來聊聊多生產者多消費者模式,也就是多條生產線程配合多條消費線程。既然這樣的話我們先把上面的代碼Single_Producer_Consumer.java類修改成新類,大部分代碼不變,僅新增2條線程去跑,一條t1的生產  共享資源類KaoYaResource不作更改,代碼如下:
  1. package com.zejian.test;  
  2. /** 
  3.  * @author zejian 
  4.  * @time 2016年3月13日 上午10:35:05 
  5.  * @decrition 多生產者多消費者模式 
  6.  */  
  7. public class Mutil_Producer_Consumer {  
  8.       
  9.     public static void main(String[] args)   
  10.     {  
  11.         KaoYaResource r = new KaoYaResource();  
  12.         Mutil_Producer pro = new Mutil_Producer(r);  
  13.         Mutil_Consumer con = new Mutil_Consumer(r);  
  14.         //生產者線程  
  15.         Thread t0 = new Thread(pro);  
  16.         Thread t1 = new Thread(pro);  
  17.         //消費者線程  
  18.         Thread t2 = new Thread(con);  
  19.         Thread t3 = new Thread(con);  
  20.         //啟動線程  
  21.         t0.start();  
  22.         t1.start();  
  23.         t2.start();  
  24.         t3.start();  
  25.     }  
  26. }  
  27. /** 
  28.  * @author zejian 
  29.  * @time 2016年3月12日 下午11:02:22 
  30.  * @decrition 生產者線程 
  31.  */  
  32. class Mutil_Producer implements Runnable  
  33. {  
  34.     private KaoYaResource r;  
  35.     Mutil_Producer(KaoYaResource r)  
  36.     {  
  37.         this.r = r;  
  38.     }  
  39.     public void run()  
  40.     {  
  41.         while(true)  
  42.         {  
  43.             r.product("北京烤鴨");  
  44.         }  
  45.     }  
  46. }  
  47. /** 
  48.  * @author zejian 
  49.  * @time 2016年3月12日 下午11:02:05 
  50.  * @decrition 消費者線程 
  51.  */  
  52. class Mutil_Consumer implements Runnable  
  53. {  
  54.     private KaoYaResource r;  
  55.     Mutil_Consumer(KaoYaResource r)  
  56.     {  
  57.         this.r = r;  
  58.     }  
  59.     public void run()  
  60.     {  
  61.         while(true)  
  62.         {  
  63.             r.consume();  
  64.         }  
  65.     }  
  66. }  
就多了兩條線程,我們運行代碼看看,結果如下:

不對呀,我們才生產一只烤鴨,怎么就被消費了3次啊,有的烤鴨生產了也沒有被消費???難道共享數(shù)據(jù)源沒有進行線程同步?我們再看看之前的KaoYaResource.java
  1. package com.zejian.test;  
  2. /** 
  3.  * @author zejian 
  4.  * @time 2016年3月12日 下午10:44:25 
  5.  * @decrition 烤鴨資源 
  6.  */  
  7. public class KaoYaResource {  
  8.       
  9.     private String name;  
  10.     private int count = 1;//烤鴨的初始數(shù)量  
  11.     private boolean flag = false;//判斷是否有需要線程等待的標志  
  12.       
  13.     /** 
  14.      * 生產烤鴨 
  15.      */  
  16.     public synchronized void product(String name){  
  17.         if(flag){  
  18.             //此時有烤鴨,等待  
  19.             try {  
  20.                 this.wait();  
  21.             } catch (InterruptedException e) {  
  22.                 e.printStackTrace();  
  23.             }  
  24.         }  
  25.         this.name=name+count;//設置烤鴨的名稱  
  26.         count++;  
  27.         System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
  28.         flag=true;//有烤鴨后改變標志  
  29.         notifyAll();//通知消費線程可以消費了  
  30.     }  
  31.       
  32.     /** 
  33.      * 消費烤鴨 
  34.      */  
  35.     public synchronized void consume(){  
  36.         if(!flag){//如果沒有烤鴨就等待  
  37.             try{this.wait();}catch(InterruptedException e){}  
  38.         }  
  39.         System.out.println(Thread.currentThread().getName()+"...消費者........"+this.name);//消費烤鴨1  
  40.         flag = false;  
  41.         notifyAll();//通知生產者生產烤鴨  
  42.     }  
  43. }  
共享數(shù)據(jù)count的獲取方法都進行synchronized關鍵字同步了呀!那怎么還會出現(xiàn)數(shù)據(jù)混亂的現(xiàn)象啊?
分析:確實,我們對共享數(shù)據(jù)也采用了同步措施,而且也應用了等待/通知機制,但是這樣的措施只在單生產者單消費者的情況下才能正確應用,但從運行結果來看,我們之前的單生產者單消費者安全處理措施就不太適合多生產者多消費者的情況了。那么問題出在哪里?可以明確的告訴大家,肯定是在資源共享類,下面我們就來分析問題是如何出現(xiàn),又該如何解決?直接上圖

解決后的資源代碼如下只將if改為了while:
  1. package com.zejian.test;  
  2. /** 
  3.  * @author zejian 
  4.  * @time 2016年3月12日 下午10:44:25 
  5.  * @decrition 烤鴨資源 
  6.  */  
  7. public class KaoYaResource {  
  8.       
  9.     private String name;  
  10.     private int count = 1;//烤鴨的初始數(shù)量  
  11.     private boolean flag = false;//判斷是否有需要線程等待的標志  
  12.     /** 
  13.      * 生產烤鴨 
  14.      */  
  15.     public synchronized void product(String name){  
  16.         while(flag){  
  17.             //此時有烤鴨,等待  
  18.             try {  
  19.                 this.wait();  
  20.             } catch (InterruptedException e) {  
  21.                 e.printStackTrace();  
  22.             }  
  23.         }  
  24.         this.name=name+count;//設置烤鴨的名稱  
  25.         count++;  
  26.         System.out.println(Thread.currentThread().getName()+"...生產者..."+this.name);  
  27.         flag=true;//有烤鴨后改變標志  
  28.         notifyAll();//通知消費線程可以消費了  
  29.     }  
  30.       
  31.     /** 
  32.      * 消費烤鴨 
  33.      */  
  34.     public synchronized void consume(){  
  35.         while(!flag){//如果沒有烤鴨就等待  
  36.             try{this.wait();}catch(InterruptedException e){}  
  37.         }  
  38.         System.out.println(Thread.currentThread().getName()+"...消費者........"+this.name);//消費烤鴨1  
  39.         flag = false;  
  40.         notifyAll();//通知生產者生產烤鴨  
  41.     }  
  42. }  
運行代碼,結果如下:

到此,多消費者多生產者模式也完成,不過上面用的是synchronied關鍵字實現(xiàn)的,而鎖對象的解決方法也一樣將之前單消費者單生產者的資源類中的if判斷改為while判斷即可代碼就不貼了哈。不過下面我們將介紹一種更有效的鎖對象解決方法,我們準備使用兩組條件對象(Condition也稱為監(jiān)視器)來實現(xiàn)等待/通知機制,也就是說通過已有的鎖獲取兩組監(jiān)視器,一組監(jiān)視生產者,一組監(jiān)視消費者。有了前面的分析這里我們直接上代碼:
  1. package com.zejian.test;  
  2. import java.util.concurrent.locks.Condition;  
  3. import java.util.concurrent.locks.Lock;  
  4. import java.util.concurrent.locks.ReentrantLock;  
  5. /** 
  6.  * @author zejian 
  7.  * @time 2016年3月13日 下午12:03:27 
  8.  * @decrition 通過已有的鎖獲取兩組監(jiān)視器,一組監(jiān)視生產者,一組監(jiān)視消費者。 
  9.  */  
  10. public class ResourceBy2Condition {  
  11.     private String name;  
  12.     private int count = 1;  
  13.     private boolean flag = false;  
  14.       
  15.     //創(chuàng)建一個鎖對象。  
  16.     Lock lock = new ReentrantLock();  
  17.       
  18.     //通過已有的鎖獲取兩組監(jiān)視器,一組監(jiān)視生產者,一組監(jiān)視消費者。  
  19.     Condition producer_con = lock.newCondition();  
  20.     Condition consumer_con = lock.newCondition();  
  21.       
  22.     /** 
  23.      * 生產 
  24.      * @param name 
  25.      */  
  26.     public  void product(String name)  
  27.     {  
  28.         lock.lock();  
  29.         try  
  30.         {  
  31.             while(flag){  
  32.                 try{producer_con.await();}catch(InterruptedException e){}  
  33.             }  
  34.             this.name = name + count;  
  35.             count++;  
  36.             System.out.println(Thread.currentThread().getName()+"...生產者5.0..."+this.name);  
  37.             flag = true;  
  38. //          notifyAll();  
  39. //          con.signalAll();  
  40.             consumer_con.signal();//直接喚醒消費線程  
  41.         }  
  42.         finally  
  43.         {  
  44.             lock.unlock();  
  45.         }  
  46.     }  
  47.       
  48.     /** 
  49.      * 消費 
  50.      */  
  51.     public  void consume()  
  52.     {  
  53.         lock.lock();  
  54.         try  
  55.         {  
  56.             while(!flag){  
  57.                 try{consumer_con.await();}catch(InterruptedException e){}  
  58.             }  
  59.             System.out.println(Thread.currentThread().getName()+"...消費者.5.0......."+this.name);//消費烤鴨1  
  60.             flag = false;  
  61. //          notifyAll();  
  62. //          con.signalAll();  
  63.             producer_con.signal();//直接喚醒生產線程  
  64.         }  
  65.         finally  
  66.         {  
  67.             lock.unlock();  
  68.         }  
  69.     }  
  70. }  
從代碼中可以看到,我們創(chuàng)建了producer_con 和consumer_con兩個條件對象,分別用于監(jiān)聽生產者線程和消費者線程,在product()方法中,我們獲取到鎖后,
如果此時flag為true的話,也就是此時還有烤鴨未被消費,因此生產線程需要等待,所以我們調用生產線程的監(jiān)控producer_con的
await()的方法進入阻塞等待池;但如果此時的flag為false的話,就說明烤鴨已經消費完,需要生產線程去生產烤鴨,那么生產線程將進行烤
鴨生產并通過消費線程的監(jiān)控器consumer_con的signal()方法去通知消費線程對烤鴨進行消費。consume()方法也是同樣的道理,這里就不
過多分析了。我們可以發(fā)現(xiàn)這種方法比我們之前的synchronized同步方法或者是單監(jiān)視器的鎖對象都來得高效和方便些,之前都是使用
notifyAll()和signalAll()方法去喚醒池中的線程,然后讓池中的線程又進入 競爭隊列去搶占CPU資源,這樣不僅喚醒了無關的線程而且又讓全
部線程進入了競爭隊列中,而我們最后使用兩種監(jiān)聽器分別監(jiān)聽生產者線程和消費者線程,這樣的方式恰好解決前面兩種方式的問題所在,
我們每次喚醒都只是生產者線程或者是消費者線程而不會讓兩者同時喚醒,這樣不就能更高效得去執(zhí)行程序了嗎?好了,到此多生產者多消
費者模式也分析完畢。
5.線程死鎖
現(xiàn)在我們再來討論一下線程死鎖問題,從上面的分析,我們知道鎖是個非常有用的工具,運用的場景非常多,因為它使用起來非常簡單,而
且易于理解。但同時它也會帶來一些不必要的麻煩,那就是可能會引起死鎖,一旦產生死鎖,就會造成系統(tǒng)功能不可用。我們先通過一個例
子來分析,這個例子會引起死鎖,使得線程t1和線程t2互相等待對方釋放鎖。
  1. package com.zejian.test;  
  2. /** 
  3.  * @author zejian 
  4.  * @time 2016年3月13日 下午2:45:52 
  5.  * @decrition 死鎖示例 
  6.  */  
  7. public class DeadLockDemo {  
  8.       
  9.     private static String A="A";  
  10.     private static String B="B";  
  11.               
  12.     public static void main(String[] args) {  
  13.         DeadLockDemo deadLock=new DeadLockDemo();  
  14.         while(true){  
  15.             deadLock.deadLock();  
  16.         }  
  17.     }  
  18.       
  19.     private void deadLock(){  
  20.         Thread t1=new Thread(new Runnable(){  
  21.             @SuppressWarnings("static-access")  
  22.             @Override  
  23.             public void run() {  
  24.                 synchronized (A) {  
  25.                     try {  
  26.                         Thread.currentThread().sleep(2000);  
  27.                     } catch (InterruptedException e) {  
  28.                         e.printStackTrace();  
  29.                     }  
  30.                 }  
  31.                 synchronized(B){  
  32.                     System.out.println("1");  
  33.                 }  
  34.             }  
  35.         });  
  36.           
  37.         Thread t2 =new Thread(new Runnable() {    
  38.             @Override  
  39.             public void run() {  
  40.                 synchronized (B) {  
  41.                     synchronized (A) {  
  42.                         System.out.println("2");  
  43.                     }  
  44.                 }  
  45.             }  
  46.         });  
  47.           
  48.         //啟動線程  
  49.         t1.start();  
  50.         t2.start();  
  51.     }     
  52. }  
同步嵌套是產生死鎖的常見情景,從上面的代碼中我們可以看出,當t1線程拿到鎖A后,睡眠2秒,此時線程t2剛好拿到了B鎖,接著要獲取A鎖,但是此時A鎖正好被t1線程持有,因此只能等待t1線程釋放鎖A,但遺憾的是在t1線程內又要求獲取到B鎖,而B鎖此時又被t2線程持有,到此結果就是t1線程拿到了鎖A同時在等待t2線程釋放鎖B,而t2線程獲取到了鎖B也同時在等待t1線程釋放鎖A,彼此等待也就造成了線程死鎖問題。雖然我們現(xiàn)實中一般不會向上面那么寫出那樣的代碼,但是有些更為復雜的場景中,我們可能會遇到這樣的問題,比如t1拿了鎖之后,因為一些異常情況沒有釋放鎖(死循環(huán)),也可能t1拿到一個數(shù)據(jù)庫鎖,釋放鎖的時候拋出了異常,沒有釋放等等,所以我們應該在寫代碼的時候多考慮死鎖的情況,這樣才能有效預防死鎖程序的出現(xiàn)。下面我們介紹一下避免死鎖的幾個常見方法:
1.避免一個線程同時獲取多個鎖。
2.避免在一個資源內占用多個 資源,盡量保證每個鎖只占用一個資源。
3.嘗試使用定時鎖,使用tryLock(timeout)來代替使用內部鎖機制。
4.對于數(shù)據(jù)庫鎖,加鎖和解鎖必須在一個數(shù)據(jù)庫連接里,否則會出現(xiàn)解鎖失敗的情況。
5.避免同步嵌套的發(fā)生
6.Thread.join()
如果一個線程A執(zhí)行了thread.join()語句,其含義是:當前線程A等待thread線程終止之后才能從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個具備超時特性的方法。這兩個超時的方法表示,如果線程在給定的超時時間里沒有終止,那么將會從該超時方法中返回。下面給出一個例子,創(chuàng)建10個線程,編號0~9,每個線程調用錢一個線程的join()方法,也就是線程0結束了,線程1才能從join()方法中返回,而0需要等待main線程結束。
  1. package com.zejian.test;  
  2. /** 
  3.  * @author zejian 
  4.  * @time 2016年3月13日 下午4:10:03 
  5.  * @decrition join案例 
  6.  */  
  7. public class JoinDemo {  
  8.       
  9.     public static void main(String[] args) {  
  10.         Thread previous = Thread.currentThread();  
  11.         for(int i=0;i<10;i++){  
  12.             //每個線程擁有前一個線程的引用。需要等待前一個線程終止,才能從等待中返回  
  13.             Thread thread=new Thread(new Domino(previous),String.valueOf(i));  
  14.             thread.start();  
  15.             previous=thread;  
  16.         }  
  17.         System.out.println(Thread.currentThread().getName()+" 線程結束");  
  18.     }  
  19. }  
  20. class Domino implements Runnable{  
  21.     private Thread thread;  
  22.     public Domino(Thread thread){  
  23.         this.thread=thread;  
  24.     }  
  25.       
  26.     @Override  
  27.     public void run() {  
  28.         try {  
  29.             thread.join();  
  30.         } catch (InterruptedException e) {  
  31.             e.printStackTrace();  
  32.         }  
  33.         System.out.println(Thread.currentThread().getName()+" 線程結束");  
  34.     }  
  35.       
  36. }  
好了,到此本篇結束。

    本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發(fā)布,不代表本站觀點。請注意甄別內容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內容,請點擊一鍵舉報。
    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多