Java併發程式設計系列之八 wait notify 和notifyAll
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
一個執行緒修改一個物件的值,而另一個執行緒則感知到了變化,然後進行相應的操作,這就是wait()、notify()和notifyAll()方法的本質。具體體現到方法上則是這樣的:一個執行緒A呼叫了物件obj的wait方法進入到等待狀態,而另一個執行緒呼叫了物件obj的notify()或者notifyAll()方法,執行緒A收到通知後從物件obj的wait方法返回,繼續執行後面的操作。
可以看到以上兩個執行緒通過物件obj進行操作,而wait和notify/notifyAll的關係就像開關訊號一樣,用來完成等待方和通知方之間的互動工作。
下面的程式碼演示了這個過程:分別建立一個等待執行緒和一個通知執行緒,前者檢查flag的值是否為false,如果符合要求就進行後續的操作,否則在lock上等待。後者在睡眠一段時間後對lock進行通知,等待執行緒這樣就可以從wait方法返回了
package com.rhwayfun.concurrency;import java.text.DateFormat;import java.text.SimpleDateFormat;import java.util.Date;/** * Created by rhwayfun on 16-4-2. */public class WaitNotifyThread { //條件是否滿足的標誌 private static boolean flag = true; //物件的監視器鎖 private static Object lock = new Object(); //日期格式化器 private static DateFormat format = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args){ Thread waitThread = new Thread(new WaitThread(),"WaitThread"); waitThread.start(); SleepUtil.second(1); Thread notifyThread = new Thread(new NotifyThread(),"NotifyThread"); notifyThread.start(); } /** * 等待執行緒 */ private static class WaitThread implements Runnable{ public void run() { //加鎖,持有物件的監視器鎖 synchronized (lock){ //只有成功獲取物件的監視器才能進入這裡 //當條件不滿足的時候,繼續wait,直到某個執行緒執行了通知 //並且釋放了lock的監視器(簡單來說就是鎖)才能從wait //方法返回 while (flag){ try { System.out.println(Thread.currentThread().getName() + " flag is true,waiting at " + format.format(new Date())); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //條件滿足,繼續工作 System.out.println(Thread.currentThread().getName() + " flag is false,running at " + format.format(new Date())); } } } /** * 通知執行緒 */ private static class NotifyThread implements Runnable{ public void run() { synchronized (lock){ //獲取lock鎖,然後執行通知,通知的時候不會釋放lock鎖 //只有當前執行緒退出了lock後,waitThread才有可能從wait返回 System.out.println(Thread.currentThread().getName() + " holds lock. Notify waitThread at " + format.format(new Date())); lock.notifyAll(); flag = false; SleepUtil.second(5); } //再次加鎖 synchronized (lock){ System.out.println(Thread.currentThread().getName() + " holds lock again. NotifyThread will sleep at " + format.format(new Date())); SleepUtil.second(5); } } }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
以上程式碼的輸出結果為:
其實使用wait、notify/notifyAll很簡單,但是仍然需要注意以下幾點:
- 使用wait()、notify()和notifyAll()時需要首先對呼叫物件加鎖
- 呼叫wait()方法後,執行緒狀態會從RUNNING變為WAITING,並將當執行緒加入到lock物件的等待佇列中
- 呼叫notify()或者notifyAll()方法後,等待在lock物件的等待佇列的執行緒不會馬上從wait()方法返回,必須要等到呼叫notify()或者notifyAll()方法的執行緒將lock鎖釋放,等待執行緒才有機會從等待佇列返回。這裡只是有機會,因為鎖釋放後,等待執行緒會出現競爭,只有競爭到該鎖的執行緒才會從wait()方法返回,其他的執行緒只能繼續等待
- notify()方法將等待佇列中的一個執行緒移到lock物件的同步佇列,notifyAll()方法則是將等待佇列中所有執行緒移到lock物件的同步佇列,被移動的執行緒的狀態由WAITING變為BLOCKED
- wait()方法上等待鎖,可以通過wait(long timeout)設定等待的超時時間
上一篇文章還有正確恢復執行緒的問題需要解決,因為通過使用wait()、notify()和notifyAll()可以很好恢復與掛起執行緒,下面是改進的程式碼:
package com.rhwayfun.concurrency;import java.text.DateFormat;import java.text.SimpleDateFormat;import java.util.Date;import java.util.concurrent.TimeUnit;/** * Created by rhwayfun on 16-4-2. */public class SafeResumeAndSuspendThread { private static DateFormat format = new SimpleDateFormat("HH:mm:ss"); //物件鎖 private static Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Runner r = new Runner(); Thread runThread = new Thread(r,"CountThread"); runThread.start(); //主執行緒休眠一會,讓CountThread有機會執行 TimeUnit.SECONDS.sleep(2); for (int i = 0; i < 3; i++){ //讓執行緒掛起 r.suspendRequest(); //讓計數執行緒掛起兩秒 TimeUnit.SECONDS.sleep(2); //看看i的值 System.out.println("after suspend, i = " + r.getValue()); //恢復執行緒的執行 r.resumeRequest(); //執行緒休眠一會 TimeUnit.SECONDS.sleep(1); } //退出程式 System.exit(0); } /** * 該執行緒是一個計數執行緒 */ private static class Runner implements Runnable{ //變數i private volatile long i; //是否繼續執行的標誌 //這裡使用volatile關鍵字可以保證多執行緒併發訪問該變數的時候 //其他執行緒都可以感知到該變數值的變化。這樣所有執行緒都會從共享 //記憶體中取值 private volatile boolean suspendFlag; public void run() { try { suspendFlag = false; i = 0; work(); } catch (InterruptedException e) { e.printStackTrace(); } } private void work() throws InterruptedException { while (true){ //只有當執行緒掛起的時候才會執行這段程式碼 waitWhileSuspend(); i++; System.out.println("calling work method, i = " + i); //只有當執行緒掛起的時候才會執行這段程式碼 waitWhileSuspend(); //休眠1秒 TimeUnit.SECONDS.sleep(1); } } /** * 忙等待 * @throws InterruptedException */ private void waitWhileSuspend() throws InterruptedException { /*while (suspendFlag){ TimeUnit.SECONDS.sleep(1); }*/ /** * 等待通知的方式才是最佳選擇 */ synchronized (lock){ while (suspendFlag){ System.out.println(Thread.currentThread().getName() + " suspend at " + format.format(new Date())); lock.wait(); } } } //讓執行緒終止的方法 public void resumeRequest(){ synchronized (lock){ try { suspendFlag = false; System.out.print("after call resumeRequest method, i = " + getValue() + ". "); lock.notifyAll(); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } public void suspendRequest(){ suspendFlag = true; System.out.print("after call suspendRequest method, i = " + getValue() + ". "); } public long getValue(){ return i; } }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
程式碼的執行結果如下:
可以看到不管是掛起還是恢復,得到的結果都是正確的,在使用等待/通知機制實現的時候,需要注意必須使用同一個lock物件作為兩個執行緒溝通的橋樑,由於synchronized關鍵字的可重入性(這點後面還會提到),保證了整個程式的正常執行。
總結:正確掛起和恢復執行緒的方法是使用boolean變數做為標誌位,能夠在合適的時間和位置正確恢復與掛起執行緒。