Java多線程編程--(3)線程互斥、同步的理解

from  http://blog.csdn.net/drifterj/article/details/7771230

多線程並行編程中,線程間同步與互斥是一個很有技巧的也很容易出錯的地方。

線程間互斥應對的是這種場景:多個線程操作同一個資源(即某個對象),爲保證線程在對資源的狀態(即對象的成員變量)進行一些非原子性操作後,狀態仍然是正確的。典型的例子是“售票廳售票應用”。售票廳剩餘100張票,10個窗口去賣這些票。這10個窗口,就是10條線程,售票廳就是他們共同操作的資源,其中剩餘的100張票就是這個資源的一個狀態。線程買票的過程就是去遞減這個剩餘數量的過程。不進行互斥控制的代碼如下:

  1. package cn.test;  
  2.   
  3. public class TicketOffice {  
  4.       
  5.     private int ticketNum = 0;  
  6.   
  7.     public TicketOffice(int ticketNum) {  
  8.         super();  
  9.         this.ticketNum = ticketNum;  
  10.     }  
  11.       
  12.     public int getTicketNum() {  
  13.         return ticketNum;  
  14.     }  
  15.   
  16.     public void setTicketNum(int ticketNum) {  
  17.         this.ticketNum = ticketNum;  
  18.     }  
  19.       
  20.     /** 
  21.      *  售票廳賣票的方法,這個方法操作了售票廳對象唯一的狀態--剩餘火車票數量。 
  22.      *  該方法此處並未進行互斥控制。 
  23.      */  
  24.     public void sellOneTicket(){  
  25.           
  26.         ticketNum--;  
  27.         // 打印剩餘票的數量  
  28.         if(ticketNum >= 0){  
  29.               
  30.             System.out.println("售票成功,剩餘票數: " + ticketNum);  
  31.         }else{  
  32.               
  33.             System.out.println("售票失敗,票已售罄!");  
  34.         }  
  35.           
  36.     }  
  37.       
  38.     public static void main(String[] args) {  
  39.           
  40.         final TicketOffice ticketOffice = new TicketOffice(100);  
  41.           
  42.         // 啓動10個線程,即10個窗口開始賣票  
  43.         for(int i=0;i<10;i++){  
  44.               
  45.             new Thread(new Runnable(){  
  46.   
  47.                 @Override  
  48.                 public void run() {  
  49.                       
  50.                     // 當還有剩餘票的時候,就去執行  
  51.                     while(ticketOffice.getTicketNum() > 0){  
  52.                           
  53.                         ticketOffice.sellOneTicket();  
  54.                     }  
  55.                       
  56.                 }  
  57.                   
  58.             }).start();  
  59.         }  
  60.     }  
  61.   
  62. }  

最後打印的部分結果如下:

  1. 售票成功,剩餘票數: 93  
  2. 售票成功,剩餘票數: 92  
  3. 售票成功,剩餘票數: 91  
  4. 售票成功,剩餘票數: 95  
  5. 售票成功,剩餘票數: 96  
  6. 售票成功,剩餘票數: 87  
  7. 售票成功,剩餘票數: 86  
  8. 售票成功,剩餘票數: 88  
  9. 售票成功,剩餘票數: 89  
  10. 售票成功,剩餘票數: 83  
  11. 售票成功,剩餘票數: 82  
  12. 售票成功,剩餘票數: 81  
  13. 售票成功,剩餘票數: 90  
  14. 售票成功,剩餘票數: 79  
  15. 售票成功,剩餘票數: 93  

 

可以看到售票廳資源的狀態:剩餘票的數量,是不正確的。數量忽大忽小,這就是對統一資源進行操作沒有控制互斥的結果。
互斥操作的控制,Java提供了關鍵字synchronized進行的。synchronized可以修飾方法,也可以修飾代碼段。其代表的含義就是:進入他修飾的這段代碼內的線程必須先去獲取一個特定對象的鎖定標示,並且虛擬機保證這個標示一次只能被一條線程擁有。通過這兩種方式修改上述代碼的方法sellOneTicket(),如下:

  1. /** 
  2.      *  已經進行了互斥控制。這裏是通過synchronized修飾整個方法實現的。 
  3.      *  線程想進入這個方法,必須獲取當前對象的鎖定表示! 
  4.      */  
  5.     public synchronized void sellOneTicket(){  
  6.           
  7.         ticketNum--;  
  8.         // 打印剩餘票的數量  
  9.         if(ticketNum >= 0){  
  10.               
  11.             System.out.println("售票成功,剩餘票數: " + ticketNum);  
  12.         }else{  
  13.               
  14.             System.out.println("售票失敗,票已售罄!");  
  15.         }  
  16.           
  17.     }  
  18.       
  19.     /** 
  20.      *  已經進行了互斥控制。這裏是通過synchronized修飾代碼塊實現的。線程要想進入修飾的代碼塊, 
  21.      *  必須獲取lock對象的對象標示。 
  22.      */  
  23.     private Object lock = new Object();  //確保整個過程中lock對象不能被改變
  24.     public void sellOneTicket2(){  
  25.           
  26.         synchronized(lock){  
  27.               
  28.             ticketNum--;  
  29.             // 打印剩餘票的數量  
  30.             if(ticketNum >= 0){  
  31.                   
  32.                 System.out.println("售票成功,剩餘票數: " + ticketNum);  
  33.             }else{  
  34.                   
  35.                 System.out.println("售票失敗,票已售罄!");  
  36.             }  
  37.         }  
  38.           
  39.     }  


通過互斥控制後的輸出爲:非常整齊,不會出現任何狀態不對的情況。

  1. 售票成功,剩餘票數: 99  
  2. 售票成功,剩餘票數: 98  
  3. 售票成功,剩餘票數: 97  
  4. 售票成功,剩餘票數: 96  
  5. 售票成功,剩餘票數: 95  
  6. 售票成功,剩餘票數: 94  
  7. 售票成功,剩餘票數: 93  
  8. 售票成功,剩餘票數: 92  
  9. 售票成功,剩餘票數: 91  
  10. 售票成功,剩餘票數: 90  
  11. 售票成功,剩餘票數: 89  
  12. 售票成功,剩餘票數: 88  
  13. 售票成功,剩餘票數: 87  
  14. 售票成功,剩餘票數: 86  
  15. 售票成功,剩餘票數: 85  
  16. 售票成功,剩餘票數: 84  
  17. 售票成功,剩餘票數: 83  
  18. 售票成功,剩餘票數: 82  

 

同步的概念再於線程間通信,比較典型的例子就是“生產者-消費者問題”。

多個生產者和多個消費者就是多條執行線程,他們共同操作一個數據結構中的數據,數據結構中有時是沒有數據的,這個時候消費者應該處於等待狀態而不是不斷的去訪問這個數據結構。這裏就涉及到線程間通信(當然此處還涉及到互斥,這裏暫不考慮這一點),消費者線程一次消費後發現數據結構空了,就應該處於等待狀態,生產者生產數據後,就去喚醒消費者線程開始消費。生產者線程某次生產後發現數據結構已經滿了,也應該處於等待狀態,消費者消費一條數據後,就去喚醒生產者繼續生產。

實現這種線程間同步,可以通過Object類提供的wait,notify, notifyAll 3個方法去進行即可。一個簡單的生產者和消費者的例子代碼爲:

  1. package cn.test;  
  2.   
  3. public class ProducerConsumer {  
  4.       
  5.     public static void main(String[] args) {  
  6.           
  7.         final MessageQueue mq = new MessageQueue(10);  
  8.         // 創建3個生產者  
  9.         for(int p=0;p<3;p++){  
  10.               
  11.             new Thread(new Runnable(){  
  12.   
  13.                 @Override  
  14.                 public void run() {  
  15.                       
  16.                     while(true){  
  17.                           
  18.                         mq.put("消息來了!");  
  19.                         // 生產消息後,休息100毫秒  
  20.                         try {  
  21.                             Thread.currentThread().sleep(100);  
  22.                         } catch (InterruptedException e) {  
  23.                             e.printStackTrace();  
  24.                         }  
  25.                     }  
  26.                 }  
  27.                   
  28.                   
  29.             }, "Producer" + p).start();  
  30.         }  
  31.           
  32.         // 創建3個消費者  
  33.         for(int s=0;s<3;s++){  
  34.               
  35.             new Thread(new Runnable(){  
  36.   
  37.                 @Override  
  38.                 public void run() {  
  39.                       
  40.                     while(true){  
  41.                           
  42.                         mq.get();  
  43.                         // 消費消息後,休息100毫秒  
  44.                         try {  
  45.                             Thread.currentThread().sleep(100);  
  46.                         } catch (InterruptedException e) {  
  47.                             e.printStackTrace();  
  48.                         }  
  49.                     }  
  50.                 }  
  51.                   
  52.                   
  53.             }, "Consumer" + s).start();  
  54.         }  
  55.     }  
  56.       
  57.     /** 
  58.      * 內部類模擬一個消息隊列,生產者和消費者就去操作這個消息隊列 
  59.      */  
  60.     private static class MessageQueue{  
  61.           
  62.         private String[] messages;// 放置消息的數據結構  
  63.         private int opIndex; // 將要操作的位置索引  
  64.   
  65.         public MessageQueue(int size) {  
  66.               
  67.             if(size <= 0){  
  68.                   
  69.                 throw new IllegalArgumentException("消息隊列的長度至少爲1!");  
  70.             }  
  71.             messages = new String[size];  
  72.             opIndex = 0;  
  73.         }  
  74.           
  75.         public synchronized void put(String message){  
  76.               
  77.             // Java中存在線程假醒的情況,此處用while而不是用if!可以參考Java規範!  
  78.             while(opIndex == messages.length){  
  79.                   
  80.                 // 消息隊列已滿,生產者需要等待  
  81.                 try {  
  82.                     wait();  
  83.                 } catch (InterruptedException e) {  
  84.                     e.printStackTrace();  
  85.                 }  
  86.             }  
  87.             messages[opIndex] = message;  
  88.             opIndex++;  
  89.             System.out.println("生產者 " + Thread.currentThread().getName() + " 生產了一條消息: " + message);  
  90.             // 生產後,對消費者進行喚醒  
  91.             notifyAll();  
  92.         }  
  93.           
  94.         public synchronized String get(){  
  95.               
  96.             // Java中存在線程假醒的情況,此處用while而不是用if!可以參考Java規範!  
  97.             while(opIndex == 0){  
  98.                   
  99.                 // 消息隊列無消息,消費者需要等待  
  100.                 try {  
  101.                     wait();  
  102.                 } catch (InterruptedException e) {  
  103.                     e.printStackTrace();  
  104.                 }  
  105.             }  
  106.             String message = messages[opIndex-1];  
  107.             opIndex--;  
  108.             System.out.println("消費者 " + Thread.currentThread().getName() + " 消費了一條消息: " + message);  
  109.             // 消費後,對生產者進行喚醒  
  110.             notifyAll();  
  111.             return message;  
  112.         }  
  113.           
  114.     }  
  115.   
  116. }  


一次輸出爲:

  1. 消費者 Consumer1 消費了一條消息: 消息來了!  
  2. 生產者 Producer0 生產了一條消息: 消息來了!  
  3. 消費者 Consumer0 消費了一條消息: 消息來了!  
  4. 生產者 Producer2 生產了一條消息: 消息來了!  
  5. 消費者 Consumer2 消費了一條消息: 消息來了!  
  6. 生產者 Producer1 生產了一條消息: 消息來了!  
  7. 消費者 Consumer0 消費了一條消息: 消息來了!  
  8. 生產者 Producer0 生產了一條消息: 消息來了!  
  9. 消費者 Consumer1 消費了一條消息: 消息來了!  
  10. 生產者 Producer2 生產了一條消息: 消息來了!  
  11. 消費者 Consumer2 消費了一條消息: 消息來了!  
  12. 生產者 Producer0 生產了一條消息: 消息來了!  
  13. 消費者 Consumer1 消費了一條消息: 消息來了!  
  14. 生產者 Producer1 生產了一條消息: 消息來了!  
  15. 消費者 Consumer0 消費了一條消息: 消息來了!  
  16. 生產者 Producer2 生產了一條消息: 消息來了!  
  17. 消費者 Consumer0 消費了一條消息: 消息來了!  
  18. 生產者 Producer1 生產了一條消息: 消息來了!  
  19. 生產者 Producer0 生產了一條消息: 消息來了!  
  20. 消費者 Consumer2 消費了一條消息: 消息來了!  
  21. 消費者 Consumer1 消費了一條消息: 消息來了!  
  22. 生產者 Producer1 生產了一條消息: 消息來了!  


多線程應用中,同步與互斥用的特別廣泛,這兩個是必須要理解並掌握的!

發佈了1 篇原創文章 · 獲贊 6 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章