java多線程編程的概述以及案例詳解


引子:  java編程中有時候會要求線程安全(注:多個線程同時訪問同一代碼的時候,不會產生不同的結果。編寫線程安全的代碼需要線程同步),這時候就需要進行多線程編程。從而用到線程間通信的技術。那么在java里面,線程間通信是怎么實現的?這篇文章將通過一個案例詳細分析。

文章關鍵詞: Object,wait,notify,notifyAll,鎖,同步(synchronized).

 

詳解一個經典的生產者消費者模型,其中用到了 wait和notifyAll方法。

源碼如下:

 1  2 
 3 import java.util.LinkedList;  4 import java.util.Queue;  5 
 6 public class MainTest {  7     public static void main(String[] args) {  8  test();  9  }  10 
 11     private static final long waitTime = 3000;  12 
 13     private static void test() {  14         Queue<Integer> queue = new LinkedList<>();// 隊列對象,它就是所謂的“鎖”
 15         int maxsize = 2;// 隊列中的最大元素個數限制  16 
 17         // 下面4個線程,一瞬間只能有一個線程獲得該對象的鎖,而進入同步代碼塊
 18         Producer producer = new Producer(queue, maxsize, "Producer");  19         Consumer consumer1 = new Consumer(queue, maxsize, "Consumer1");  20         Consumer consumer2 = new Consumer(queue, maxsize, "Consumer2");  21         Consumer consumer3 = new Consumer(queue, maxsize, "Consumer3");  22 
 23         // 其實隨便先啟動哪個都無所謂,因為只有一個鎖,每一次只會有一個線程能持有這個鎖,來操作queue
 24  producer.start();  25  consumer2.start();  26  consumer1.start();  27  consumer3.start();  28  }  29 
 30     /**
 31  * 生產者線程  32      */
 33     public static class Producer extends Thread {  34         Queue<Integer> queue;// queue,對象鎖
 35         int maxsize;// 貌似是隊列的最大產量
 36 
 37         Producer(Queue<Integer> queue, int maxsize, String name) {  38             this.queue = queue;  39             this.maxsize = maxsize;  40             this.setName(name);  41  }  42 
 43  @Override  44         public void run() {  45             while (true) {// 無限循環,不停生產元素,直到達到上限,只要達到上限,那就wait等待。
 46                 synchronized (queue) {// 同步代碼塊,只有持有queue這個鎖的對象才能訪問這個代碼塊
 47                     try {  48  Thread.sleep(waitTime);  49                         // sleep和wait的區別,sleep會讓當前執行的線程阻塞一段時間,但是不會釋放鎖,  50                         // 但是wait,會阻塞,並且會釋放鎖
 51                     } catch (Exception e) {  52  }  53 
 54                     System.out.println(this.getName() + "獲得隊列的鎖");// 只有你獲得了queue對象的鎖,你才能執行到這里  55                     // 條件的判斷一定要使用while而不是if
 56                     while (queue.size() == maxsize) {// 判斷生產有沒有達到上限,如果達到了上限,就讓當前線程等待
 57                         System.out.println("隊列已滿,生產者" + this.getName() + "等待");  58                         try {  59                             queue.wait();// 讓當前線程等待,直到其他線程調用notifyAll
 60                         } catch (Exception e) {  61  }  62  }  63 
 64                     // 下面寫的就是生產過程
 65                     int num = (int) (Math.random() * 100);  66                     queue.offer(num);// 將一個int數字插入到隊列中
 67 
 68                     System.out.println(this.getName() + "生產一個元素:" + num);  69                     // 喚醒其他線程,在這里案例中是 "等待中"的消費者線程
 70                     queue.notifyAll();// (注:notifyAll的作用是  71                                         // 喚醒所有持有queue對象鎖的正在等待的線程)
 72 
 73                     System.out.println(this.getName() + "退出一次生產過程!");  74  }  75  }  76  }  77  }  78 
 79     public static class Consumer extends Thread {  80         Queue<Integer> queue;  81         int maxsize;  82 
 83         Consumer(Queue<Integer> queue, int maxsize, String name) {  84             this.queue = queue;  85             this.maxsize = maxsize;  86             this.setName(name);  87  }  88 
 89  @Override  90         public void run() {  91             while (true) {  92                 synchronized (queue) {// 要想進入下面的代碼,就必須先獲得鎖。
 93                     try {  94                         Thread.sleep(waitTime);// sleep,讓當前線程阻塞指定時長,但是並不會釋放queue鎖
 95                     } catch (Exception e) {  96  }  97 
 98                     System.out.println(this.getName() + "獲得隊列的鎖");// 拿到了鎖,才能執行到這里  99                     // 條件的判斷一定要使用while而不是if,
100                     while (queue.isEmpty()) {// while判斷隊列是否為空,如果為空,當前消費者線程就必須wait,等生產者先生產元素 101                         // 這里,消費者有多個(因為有多個consumer線程),每一個消費者如果發現了隊列空了,就會wait。
102                         System.out.println("隊列為空,消費者" + this.getName() + "等待"); 103                         try { 104  queue.wait(); 105                         } catch (Exception e) { 106  } 107  } 108 
109                     // 如果隊列不是空,那么就彈出一個元素
110                     int num = queue.poll(); 111                     System.out.println(this.getName() + "消費一個元素:" + num); 112                     queue.notifyAll();// 然后再喚醒所有線程,喚醒不會釋放自己的鎖
113 
114                     System.out.println(this.getName() + "退出一次消費過程!"); 115  } 116  } 117  } 118  } 119 }

案例解析:

1)此案例模擬的是,生產者線程 生產元素並且插入到Queue中,Queue有一個存儲個數的限制。消費者線程,從Queue中拿出元素。兩個線程都是無限循環執行的。

2)在生產者線程的生產過程(隨機產生一個int然后插入到queue中)執行之前,首先檢查Queue的存儲個數有沒有到達上限,如果到達了,那就不能生產,代碼中調用了queue.wait();來使生產者線程進入等待狀態並且釋放鎖。如果沒超過,那就反復執行,直到到達上限。

3)消費者線程在執行消費過程(從queue中彈出一個元素)執行之前,首先要檢查queue是不是空,如果是空,那就不能消費,調用queue.wait()讓消費線程進入等待狀態並且釋放鎖。

4)在生產過程 或 消費過程執行完畢之后,都會有queue.notifyAll();來喚醒等待鎖的所有線程。

5)生產者中,判定queue的元素個數是不是到達上限。以及 消費者中,判定queue是不是空,這種判定queue.wait()的條件 所使用的關鍵字,並不是if,而是while.

因為在執行了wait之后,該線程的執行,會暫時停留在這個while循環中,等待被喚醒,一旦被喚醒,while循環會繼續執行,從而會再次判斷條件是否滿足。

6)代碼中能找到Thread.sleep(long);方法,它的作用,是當當前線程阻塞指定時間,但是它並不會釋放鎖。而wait除了阻塞之外,還會釋放鎖。

 

 案例執行的結果打印:

Producer獲得隊列的鎖
Producer生產一個元素:86
Producer退出一次生產過程!
Producer獲得隊列的鎖
Producer生產一個元素:31
Producer退出一次生產過程!
Producer獲得隊列的鎖
隊列已滿,生產者Producer等待
Consumer2獲得隊列的鎖
Consumer2消費一個元素:86
Consumer2退出一次消費過程!
Consumer3獲得隊列的鎖
Consumer3消費一個元素:31
Consumer3退出一次消費過程!
Consumer1獲得隊列的鎖
隊列為空,消費者Consumer1等待
Consumer3獲得隊列的鎖
隊列為空,消費者Consumer3等待
Consumer2獲得隊列的鎖
隊列為空,消費者Consumer2等待
Producer生產一個元素:29
Producer退出一次生產過程!
Producer獲得隊列的鎖
Producer生產一個元素:82
Producer退出一次生產過程!
Producer獲得隊列的鎖
隊列已滿,生產者Producer等待
Consumer2消費一個元素:29
Consumer2退出一次消費過程!

結果分析(請對照日志來看,大神請繞道,下面的描述比較啰嗦):

由於首先啟動的是生產者線程(Producer),所以producer先獲得了鎖,進行了兩次生產。再次嘗試生產的時候發現queue滿了,於是,生產者進入等待。

之后,consumer2的得到了鎖,於是進行消費,消費執行了一次,鎖被consumer3奪走,consumer3執行了一次消費。

之后,consumer1得到了鎖,就當它准備開始消費的時候,發現queue空了,不能消費了,於是代碼調用queue.wait().來讓consumer1進入等待。

之后,consumer3和consumer2相繼得到鎖,但是他們都發現,queue空了,也不能消費,於是同樣調用queue.wait()來讓consumer3和consumer1進入等待。

再然后,生產者得到了鎖(這里可能很奇怪,生產者不是在等待么?它什么時候被喚醒的,查看Consumer的代碼,能發現,在每一次成功消費之后,都會有queue.notifyAll(),也就是說,在之前cunsumer2消費之后,生產者就已經被喚醒了,只是他沒有得到鎖,所以就沒有執行生產過程)。

生產者得到鎖之后,繼續while循環,發現queue並沒有填滿,於是進入生產過程。之后···就是無限循環了。

 

這種模型在線程安全比較高的場景中,會被經常用到,比如買票系統,同一張票不能被賣兩次。所以,這張票,在同一時間只能被一個線程訪問。

-------------------

 

 

案例解析完畢,但是針對java多線程,也許有人會有其他疑問,下面列舉幾個比較重要的問題加以說明:

問:在java中,wait,notify以及notifyAll是用來做線程之間的通信的,但是為什么這3個方法不是在Thread類里面,而是在Object類里面?
答:  

這3個方法雖然是用於線程間的通信,但是他們並不是直接就在Thread類里面,而是在Object類。

這是 因為 調用一個Object的wait,notify,notifyAll 必須保證該段代碼對於該Object是同步的, 否則就可能會報異常IllegalMonitorStateException(具體可以進入Object類的源碼搜索此異常,注釋中有詳細說明),通常的寫法如下,

synchronized(obj){//在執行wait,notify,notifyAll時,必須保證這段代碼持有obj對象的鎖。
  obj.wait();
  ...
  obj.notify();
  ...
  obj.notifyAll();
}

如果多個線程都寫了上面的代碼,那么同一時間,只會有一個線程能獲取obj對象鎖。

所以說,這3個方法在Object類里,而不是在Thread類里,其實是java框架的設定,通過Object鎖來完成線程間的通信。

 

問:wait,notify,notifyAll的作用分別是什么?

答:

wait-讓當前線程進入等待狀態,並且釋放鎖;

notify -喚醒任意一個正在等待鎖的線程,並且讓它得到鎖。

notifyAll,喚醒所有等待對象鎖的線程,如果有多個線程都被喚醒,那么鎖將會被他們爭奪,同一時間只會有一個線程得到鎖。

 

問:notify,notifyAll有啥區別?

答:

notify,讓任意一個等待對象鎖的線程得到鎖,並且喚醒他。

notifyAll,喚醒所有等到對象鎖的線程,如果有多個被喚醒的線程,鎖將會被爭奪,爭奪到鎖的線程就可以執行.

 

===================就寫到這里了。上面的是基礎知識,在復雜場景中可能會被復雜化千萬倍,但是萬變不離其宗,了解了原理,就能應對大部分場景了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM