https://www.cnblogs.com/moongeek/p/7631447.html
https://blog.51cto.com/12304309/2138845
1、wait()、notify/notifyAll() 方法是Object的本地final方法,無法被重寫。
2、wait()使當前線程阻塞,前提是 必須先獲得鎖,一般配合synchronized 關鍵字使用,即,一般在synchronized 同步代碼塊裏使用 wait()、notify/notifyAll() 方法。
3、 由於 wait()、notify/notifyAll() 在synchronized 代碼塊執行,說明當前線程一定是獲取了鎖的。
當線程執行wait()方法時候,會釋放當前的鎖,然後讓出CPU,進入等待狀態。
只有當 notify/notifyAll() 被執行時候,纔會喚醒一個或多個正處於等待狀態的線程,然後繼續往下執行,直到執行完synchronized 代碼塊的代碼或是中途遇到wait() ,再次釋放鎖。
也就是說,notify/notifyAll() 的執行只是喚醒沉睡的線程,而不會立即釋放鎖,鎖的釋放要看代碼塊的具體執行情況。所以在編程中,儘量在使用了notify/notifyAll() 後立即退出臨界區,以喚醒其他線程
4、wait() 需要被try catch包圍,中斷也可以使wait等待的線程喚醒。
5、notify 和wait 的順序不能錯,如果A線程先執行notify方法,B線程在執行wait方法,那麼B線程是無法被喚醒的。
6、notify 和 notifyAll的區別
notify方法只喚醒一個等待(對象的)線程並使該線程開始執行。所以如果有多個線程等待一個對象,這個方法只會喚醒其中一個線程,選擇哪個線程取決於操作系統對多線程管理的實現。notifyAll 會喚醒所有等待(對象的)線程,儘管哪一個線程將會第一個處理取決於操作系統的實現。如果當前情況下有多個線程需要被喚醒,推薦使用notifyAll 方法。比如在生產者-消費者裏面的使用,每次都需要喚醒所有的消費者或是生產者,以判斷程序是否可以繼續往下執行。
7、在多線程中要測試某個條件的變化,使用if 還是while?
要注意,notify喚醒沉睡的線程後,線程會接着上次的執行繼續往下執行。所以在進行條件判斷時候,可以先把 wait 語句忽略不計來進行考慮,顯然,要確保程序一定要執行,並且要保證程序直到滿足一定的條件再執行,要使用while來執行,以確保條件滿足和一定執行。如下代碼:
1 public class K { 2 //狀態鎖 3 private Object lock; 4 //條件變量 5 private int now,need; 6 public void produce(int num){ 7 //同步 8 synchronized (lock){ 9 //當前有的不滿足需要,進行等待 10 while(now < need){ 11 try { 12 //等待阻塞 13 wait(); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 System.out.println("我被喚醒了!"); 18 } 19 // 做其他的事情 20 } 21 } 22 } 23
顯然,只有當前值滿足需要值的時候,線程纔可以往下執行,所以,必須使用while 循環阻塞。注意,wait() 當被喚醒時候,只是讓while循環繼續往下走.如果此處用if的話,意味着if繼續往下走,會跳出if語句塊。但是,notifyAll 只是負責喚醒線程,並不保證條件云云,所以需要手動來保證程序的邏輯。
8、實現生產者和消費者問題
什麼是生產者-消費者問題呢?
如上圖,假設有一個公共的容量有限的池子,有兩種人,一種是生產者,另一種是消費者。需要滿足如下條件:
1、生產者產生資源往池子裏添加,前提是池子沒有滿,如果池子滿了,則生產者暫停生產,直到自己的生成能放下池子。
2、消費者消耗池子裏的資源,前提是池子的資源不爲空,否則消費者暫停消耗,進入等待直到池子裏有資源數滿足自己的需求。
- 倉庫類
1 import java.util.LinkedList; 2 3 /** 4 * 生產者和消費者的問題 5 * wait、notify/notifyAll() 實現 6 */ 7 public class Storage1 implements AbstractStorage { 8 //倉庫最大容量 9 private final int MAX_SIZE = 100; 10 //倉庫存儲的載體 11 private LinkedList list = new LinkedList(); 12 13 //生產產品 14 public void produce(int num){ 15 //同步 16 synchronized (list){ 17 //倉庫剩餘的容量不足以存放即將要生產的數量,暫停生產 18 while(list.size()+num > MAX_SIZE){ 19 System.out.println("【要生產的產品數量】:" + num + "\t【庫存量】:" 20 + list.size() + "\t暫時不能執行生產任務!"); 21 22 try { 23 //條件不滿足,生產阻塞 24 list.wait(); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 } 29 30 for(int i=0;i<num;i++){ 31 list.add(new Object()); 32 } 33 34 System.out.println("【已經生產產品數】:" + num + "\t【現倉儲量爲】:" + list.size()); 35 36 list.notifyAll(); 37 } 38 } 39 40 //消費產品 41 public void consume(int num){ 42 synchronized (list){ 43 44 //不滿足消費條件 45 while(num > list.size()){ 46 System.out.println("【要消費的產品數量】:" + num + "\t【庫存量】:" 47 + list.size() + "\t暫時不能執行生產任務!"); 48 49 try { 50 list.wait(); 51 } catch (InterruptedException e) { 52 e.printStackTrace(); 53 } 54 } 55 56 //消費條件滿足,開始消費 57 for(int i=0;i<num;i++){ 58 list.remove(); 59 } 60 61 System.out.println("【已經消費產品數】:" + num + "\t【現倉儲量爲】:" + list.size()); 62 63 list.notifyAll(); 64 } 65 } 66 }
- 抽象倉庫類
1 public interface AbstractStorage { 2 void consume(int num); 3 void produce(int num); 4 }
- 生產者
1 public class Producer extends Thread{ 2 //每次生產的數量 3 private int num ; 4 5 //所屬的倉庫 6 public AbstractStorage abstractStorage; 7 8 public Producer(AbstractStorage abstractStorage){ 9 this.abstractStorage = abstractStorage; 10 } 11 12 public void setNum(int num){ 13 this.num = num; 14 } 15 16 // 線程run函數 17 @Override 18 public void run() 19 { 20 produce(num); 21 } 22 23 // 調用倉庫Storage的生產函數 24 public void produce(int num) 25 { 26 abstractStorage.produce(num); 27 } 28 }
- 消費者
1 public class Consumer extends Thread{ 2 // 每次消費的產品數量 3 private int num; 4 5 // 所在放置的倉庫 6 private AbstractStorage abstractStorage1; 7 8 // 構造函數,設置倉庫 9 public Consumer(AbstractStorage abstractStorage1) 10 { 11 this.abstractStorage1 = abstractStorage1; 12 } 13 14 // 線程run函數 15 public void run() 16 { 17 consume(num); 18 } 19 20 // 調用倉庫Storage的生產函數 21 public void consume(int num) 22 { 23 abstractStorage1.consume(num); 24 } 25 26 public void setNum(int num){ 27 this.num = num; 28 } 29 }
- 測試
1 public class Test{ 2 public static void main(String[] args) { 3 // 倉庫對象 4 AbstractStorage abstractStorage = new Storage1(); 5 6 // 生產者對象 7 Producer p1 = new Producer(abstractStorage); 8 Producer p2 = new Producer(abstractStorage); 9 Producer p3 = new Producer(abstractStorage); 10 Producer p4 = new Producer(abstractStorage); 11 Producer p5 = new Producer(abstractStorage); 12 Producer p6 = new Producer(abstractStorage); 13 Producer p7 = new Producer(abstractStorage); 14 15 // 消費者對象 16 Consumer c1 = new Consumer(abstractStorage); 17 Consumer c2 = new Consumer(abstractStorage); 18 Consumer c3 = new Consumer(abstractStorage); 19 20 // 設置生產者產品生產數量 21 p1.setNum(10); 22 p2.setNum(10); 23 p3.setNum(10); 24 p4.setNum(10); 25 p5.setNum(10); 26 p6.setNum(10); 27 p7.setNum(80); 28 29 // 設置消費者產品消費數量 30 c1.setNum(50); 31 c2.setNum(20); 32 c3.setNum(30); 33 34 // 線程開始執行 35 c1.start(); 36 c2.start(); 37 c3.start(); 38 39 p1.start(); 40 p2.start(); 41 p3.start(); 42 p4.start(); 43 p5.start(); 44 p6.start(); 45 p7.start(); 46 } 47 }
- 輸出
【要消費的產品數量】:50 【庫存量】:0 暫時不能執行生產任務! 【要消費的產品數量】:20 【庫存量】:0 暫時不能執行生產任務! 【要消費的產品數量】:30 【庫存量】:0 暫時不能執行生產任務! 【已經生產產品數】:10 【現倉儲量爲】:10 【要消費的產品數量】:30 【庫存量】:10 暫時不能執行生產任務! 【要消費的產品數量】:20 【庫存量】:10 暫時不能執行生產任務! 【要消費的產品數量】:50 【庫存量】:10 暫時不能執行生產任務! 【已經生產產品數】:10 【現倉儲量爲】:20 【已經生產產品數】:10 【現倉儲量爲】:30 【要消費的產品數量】:50 【庫存量】:30 暫時不能執行生產任務! 【已經消費產品數】:20 【現倉儲量爲】:10 【要消費的產品數量】:30 【庫存量】:10 暫時不能執行生產任務! 【已經生產產品數】:10 【現倉儲量爲】:20 【要消費的產品數量】:50 【庫存量】:20 暫時不能執行生產任務! 【要消費的產品數量】:30 【庫存量】:20 暫時不能執行生產任務! 【已經生產產品數】:10 【現倉儲量爲】:30 【已經消費產品數】:30 【現倉儲量爲】:0 【要消費的產品數量】:50 【庫存量】:0 暫時不能執行生產任務! 【已經生產產品數】:10 【現倉儲量爲】:10 【要消費的產品數量】:50 【庫存量】:10 暫時不能執行生產任務! 【已經生產產品數】:80 【現倉儲量爲】:90 【已經消費產品數】:50 【現倉儲量爲】:40
我自己總結的Java學習的系統知識點以及面試問題,目前已經開源,會一直完善下去,歡迎建議和指導歡迎Star: https://github.com/Snailclimb/Java-Guide
本節思維導圖:
一 等待/通知機制介紹
1.1 不使用等待/通知機制
當兩個線程之間存在生產和消費者關係,也就是說第一個線程(生產者)做相應的操作然後第二個線程(消費者)感知到了變化又進行相應的操作。比如像下面的whie語句一樣,假設這個value值就是第一個線程操作的結果,doSomething()是第二個線程要做的事,當滿足條件value=desire後才執行doSomething()。
但是這裏有個問題就是:第二個語句不停過通過輪詢機制來檢測判斷條件是否成立。如果輪詢時間的間隔太小會浪費CPU資源,輪詢時間的間隔太大,就可能取不到自己想要的數據。所以這裏就需要我們今天講到的等待/通知(wait/notify)機制來解決這兩個矛盾。
while(value=desire){
doSomething();
}
1.2 什麼是等待/通知機制?
通俗來講:
等待/通知機制在我們生活中比比皆是,一個形象的例子就是廚師和服務員之間就存在等待/通知機制。
- 廚師做完一道菜的時間是不確定的,所以菜到服務員手中的時間是不確定的;
- 服務員就需要去“等待(wait)”;
- 廚師把菜做完之後,按一下鈴,這裏的按鈴就是“通知(nofity)”;
- 服務員聽到鈴聲之後就知道菜做好了,他可以去端菜了。
用專業術語講:
等待/通知機制,是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B調用了對象O的notify()/notifyAll()方法,線程A收到通知後退出等待隊列,進入可運行狀態,進而執行後續操作。上訴兩個線程通過對象O來完成交互,而對象上的wait()方法和notify()/notifyAll()方法的關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。
1.3 等待/通知機制的相關方法
方法名稱 | 描述 |
---|---|
notify() | 隨機喚醒等待隊列中等待同一共享資源的 “一個線程”,並使該線程退出等待隊列,進入可運行狀態,也就是notify()方法僅通知“一個線程” |
notifyAll() | 使所有正在等待隊列中等待同一共享資源的 “全部線程” 退出等待隊列,進入可運行狀態。此時,優先級最高的那個線程最先執行,但也有可能是隨機執行,這取決於JVM虛擬機的實現 |
wait() | 使調用該方法的線程釋放共享資源鎖,然後從運行狀態退出,進入等待隊列,直到被再次喚醒 |
wait(long) | 超時等待一段時間,這裏的參數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回 |
wait(long,int) | 對於超時時間更細力度的控制,可以達到納秒 |
二 等待/通知機制的實現
2.1 我的第一個等待/通知機制程序
MyList.java
public class MyList {
private static List<String> list = new ArrayList<String>();
public static void add() {
list.add("anyString");
}
public static int size() {
return list.size();
}
}
ThreadA.java
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
if (MyList.size() != 5) {
System.out.println("wait begin "
+ System.currentTimeMillis());
lock.wait();
System.out.println("wait end "
+ System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadB.java
public class ThreadB extends Thread {
private Object lock;
public ThreadB(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
MyList.add();
if (MyList.size() == 5) {
lock.notify();
System.out.println("已發出通知!");
}
System.out.println("添加了" + (i + 1) + "個元素!");
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Run.java
public class Run {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(50);
ThreadB b = new ThreadB(lock);
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運行結果:
從運行結果:"wait end 1521967322359"最後輸出可以看出,<font color="red">notify()執行後並不會立即釋放鎖。</font>下面我們會補充介紹這個知識點。
synchronized關鍵字可以將任何一個Object對象作爲同步對象來看待,而Java爲每個Object都實現了等待/通知(wait/notify)機制的相關方法,它們必須用在synchronized關鍵字同步的Object的臨界區內。通過調用wait()方法可以使處於臨界區內的線程進入等待狀態,同時釋放被同步對象的鎖。而notify()方法可以喚醒一個因調用wait操作而處於阻塞狀態中的線程,使其進入就緒狀態。被重新喚醒的線程會視圖重新獲得臨界區的控制權也就是鎖,並繼續執行wait方法之後的代碼。如果發出notify操作時沒有處於阻塞狀態中的線程,那麼該命令會被忽略。
如果我們這裏不通過等待/通知(wait/notify)機制實現,而是使用如下的while循環實現的話,我們上面也講過會有很大的弊端。
while(MyList.size() == 5){
doSomething();
}
2.2線程的基本狀態
上面幾章的學習中我們已經掌握了與線程有關的大部分API,這些API可以改變線程對象的狀態。如下圖所示:
- 新建(new):新創建了一個線程對象。
- 可運行(runnable):線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲 取cpu的使用權。
- 運行(running):可運行狀態(runnable)的線程獲得了cpu時間片(timeslice),執行程序代碼。
-
阻塞(block):阻塞狀態是指線程因爲某種原因放棄了cpu使用權,也即讓出了cpu timeslice,暫時停止運行。直到線程進入可運行(runnable)狀態,纔有 機會再次獲得cpu timeslice轉到運行(running)狀態。阻塞的情況分三種:
(一). 等待阻塞:運行(running)的線程執行o.wait()方法,JVM會把該線程放 入等待隊列(waitting queue)中。
(二). **同步阻塞**:運行(running)的線程在獲取對象的同步鎖時,若該同步鎖 被別的線程佔用,則JVM會把該線程放入鎖池(lock pool)中。
(三). **其他阻塞**: 運行(running)的線程執行Thread.sleep(long ms)或t.join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入可運行(runnable)狀態。
- 死亡(dead):線程run()、main()方法執行結束,或者因異常退出了run()方法,則該線程結束生命週期。死亡的線程不可再次復生。
備註:
可以用早起坐地鐵來比喻這個過程:
還沒起牀:sleeping
起牀收拾好了,隨時可以坐地鐵出發:Runnable
等地鐵來:Waiting
地鐵來了,但要排隊上地鐵:I/O阻塞
上了地鐵,發現暫時沒座位:synchronized阻塞
地鐵上找到座位:Running
到達目的地:Dead
2.3 notify()鎖不釋放
<font color="red">當方法wait()被執行後,鎖自動被釋放,但執行完notify()方法後,鎖不會自動釋放。必須執行完notify()方法所在的synchronized代碼塊後才釋放。</font>
下面我們通過代碼驗證一下:
(完整代碼:https://github.com/Snailclimb/threadDemo/tree/master/src/wait\_notifyHoldLock)
<font size="2">帶wait方法的synchronized代碼塊</font>
synchronized (lock) {
System.out.println("begin wait() ThreadName="
+ Thread.currentThread().getName());
lock.wait();
System.out.println(" end wait() ThreadName="
+ Thread.currentThread().getName());
}
<font size="2">帶notify方法的synchronized代碼塊</font>
synchronized (lock) {
System.out.println("begin notify() ThreadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
lock.notify();
Thread.sleep(5000);
System.out.println(" end notify() ThreadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
}
如果有三個同一個對象實例的線程a,b,c,a線程執行帶wait方法的synchronized代碼塊然後bb線程執行帶notify方法的synchronized代碼塊緊接着c執行帶notify方法的synchronized代碼塊。
<font size="2">運行效果如下:</font>
<font color="red">這也驗證了我們剛開始的結論:必須執行完notify()方法所在的synchronized代碼塊後才釋放。</font>
2.4 當interrupt方法遇到wait方法
<font color="red">當線程呈wait狀態時,對線程對象調用interrupt方法會出現InterrupedException異常。</font>
<font size="2">Service.java</font>
public class Service {
public void testMethod(Object lock) {
try {
synchronized (lock) {
System.out.println("begin wait()");
lock.wait();
System.out.println(" end wait()");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("出現異常了,因爲呈wait狀態的線程被interrupt了!");
}
}
}
<font size="2">ThreadA.java</font>
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
Service service = new Service();
service.testMethod(lock);
}
}
<font size="2">Test.java</font>
public class Test {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(5000);
a.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
<font size="2">運行結果:</font>
參考:
《Java多線程編程核心技術》
《Java併發編程的藝術》