_159_java_多執行緒之wait/notify
本文主要學習JAVA多執行緒中的 wait()方法 與 notify()/notifyAll()方法的用法。
①wait() 與 notify/notifyAll 方法必須在同步程式碼塊中使用
②wait() 與 notify/notifyAll() 的執行過程
③中斷 呼叫wait()方法進入等待佇列的 執行緒
④notify 通知的順序不能錯
⑤多執行緒中測試某個條件的變化用 if 還是用 while?
①wait() 與 notify/notifyAll 方法必須在同步程式碼塊中使用
wait() 與 notify/notifyAll() 是Object類的方法,在執行兩個方法時,要先獲得鎖。那麼怎麼獲得鎖呢?
在這篇:JAVA多執行緒之Synchronized關鍵字--物件鎖的特點文章中介紹了使用synchronized關鍵字獲得鎖。因此,wait() 與 notify/notifyAll() 經常與synchronized搭配使用,即在synchronized修飾的同步程式碼塊或方法裡面呼叫wait() 與 notify/notifyAll()方法。
②wait() 與 notify/notifyAll() 的執行過程
由於 wait() 與 notify/notifyAll() 是放在同步程式碼塊中的,因此執行緒在執行它們時,肯定是進入了臨界區中的,即該執行緒肯定是獲得了鎖的。
當執行緒執行wait()時,會把當前的鎖釋放,然後讓出CPU,進入等待狀態。
當執行notify/notifyAll方法時,會喚醒一個處於等待該 物件鎖 的執行緒,然後繼續往下執行,直到執行完退出物件鎖鎖住的區域(synchronized修飾的程式碼塊)後再釋放鎖。
從這裡可以看出,notify/notifyAll()執行後,並不立即釋放鎖,而是要等到執行完臨界區中程式碼後,再釋放。故,在實際程式設計中,我們應該儘量線上程呼叫notify/notifyAll()後,立即退出臨界區。即不要在notify/notifyAll()後面再寫一些耗時的程式碼。示例如下:
1 public class Service { 2 3 public void testMethod(Object lock) { 4 try { 5 synchronized (lock) { 6 System.out.println("begin wait() ThreadName=" 7 + Thread.currentThread().getName()); 8 lock.wait(); 9 System.out.println(" end wait() ThreadName=" 10 + Thread.currentThread().getName()); 11 } 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 } 16 17 public void synNotifyMethod(Object lock) { 18 try { 19 synchronized (lock) { 20 System.out.println("begin notify() ThreadName=" 21 + Thread.currentThread().getName() + " time=" 22 + System.currentTimeMillis()); 23 lock.notify(); 24 Thread.sleep(5000); 25 System.out.println(" end notify() ThreadName=" 26 + Thread.currentThread().getName() + " time=" 27 + System.currentTimeMillis()); 28 } 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 } 33 }
在第3行的testMethod()中呼叫 wait(),在第17行的synNotifyMethod()中呼叫notify()
從上面的程式碼可以看出,wait() 與 notify/notifyAll()都是放在同步程式碼塊中才能夠執行的。如果在執行wait() 與 notify/notifyAll() 之前沒有獲得相應的物件鎖,就會丟擲:java.lang.IllegalMonitorStateException異常。
在第8行,當ThreadA執行緒執行lock.wait();這條語句時,釋放獲得的物件鎖lock,並放棄CPU,進入等待佇列。
當另一個執行緒執行第23行lock.notify();,會喚醒ThreadA,但是此時它並不立即釋放鎖,接下來它睡眠了5秒鐘(sleep()是不釋放鎖的,事實上sleep()也可以不在同步程式碼塊中呼叫),直到第28行,退出synchronized修飾的臨界區時,才會把鎖釋放。這時,ThreadA就有機會獲得另一個執行緒釋放的鎖,並從等待的地方起(第9行)起開始執行。
接下來是兩個執行緒類,執行緒類ThreadA呼叫testMethod()方法執行lock.wait();時被掛起,另一個執行緒類synNotifyMethodThread呼叫synNotifyMethod()負責喚醒掛起的執行緒。程式碼如下:
1 public class ThreadA extends Thread { 2 private Object lock; 3 4 public ThreadA(Object lock) { 5 super(); 6 this.lock = lock; 7 } 8 9 @Override 10 public void run() { 11 Service service = new Service(); 12 service.testMethod(lock); 13 } 14 } 15 16 public class SynNotifyMethodThread extends Thread { 17 private Object lock; 18 19 public SynNotifyMethodThread(Object lock) { 20 super(); 21 this.lock = lock; 22 } 23 24 @Override 25 public void run() { 26 Service service = new Service(); 27 service.synNotifyMethod(lock); 28 } 29 }
再接下來是測試類:
1 public class Test { 2 3 public static void main(String[] args) throws InterruptedException { 4 5 Object lock = new Object(); 6 7 ThreadA a = new ThreadA(lock); 8 a.start(); 9 10 //NotifyThread notifyThread = new NotifyThread(lock); 11 // notifyThread.start(); 12 13 SynNotifyMethodThread c = new SynNotifyMethodThread(lock); 14 c.start(); 15 } 16 }
③中斷 呼叫wait()方法進入等待佇列的 執行緒
示例程式碼如下:
1 public class Service { 2 3 public void testMethod(Object lock) { 4 try { 5 synchronized (lock) { 6 System.out.println("begin wait()"); 7 lock.wait(); 8 System.out.println(" end wait()"); 9 } 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 System.out.println("出現異常"); 13 } 14 } 15 } 16 17 public class ThreadA extends Thread { 18 19 private Object lock; 20 21 public ThreadA(Object lock) { 22 super(); 23 this.lock = lock; 24 } 25 26 @Override 27 public void run() { 28 Service service = new Service(); 29 service.testMethod(lock); 30 } 31 }
注意,在第23行wait()方法是Object類的物件lock呼叫的。而下面的interrupt()方法是ThreadA類的物件呼叫的。在ThreadA裡面,將Object的物件作為引數傳給了testMethod()方法,ThreadA的run()方法去呼叫testMethod(),從而wait()使ThreadA的執行緒暫停了(暫停當前執行wait()的執行緒)。從這裡可以看出一個區別:
Object類中與執行緒有關的方法:
1)notify/notifyAll
2)wait()/wait(long)
java.lang.Thread中與之相關的方法:
1)interrupt()
2)sleep()/sleep(long)
3)join()/suspend()/resume()....
測試類程式碼如下:
1 public class Test { 2 3 public static void main(String[] args) { 4 5 try { 6 Object lock = new Object(); 7 8 ThreadA a = new ThreadA(lock); 9 a.start(); 10 11 Thread.sleep(5000); 12 13 a.interrupt(); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 } 18 }
當執行第13行的interrupt()時,處於wait中的執行緒“立即”被喚醒(一般是立即響應中斷請求),並丟擲異常。此時,執行緒也就結束了。
④notify 通知的順序不能錯
假設線上程A中執行wait(),線上程B中執行notify()。但如果執行緒B先執行了notify()然後結束了,執行緒A才去執行wait(),那此時,執行緒A將無法被正常喚醒了(還可以通過③中提到的interrupt()方法以丟擲異常的方式喚醒^~^)。
這篇文章: JAVA多執行緒之執行緒間的通訊方式中的第③點提到了notify通知順序出錯會導致 呼叫wait()進入等待佇列的執行緒再也無法被喚醒了。
⑤多執行緒中測試某個條件的變化用 if 還是用 while?
以前一直不明白 當線上程的run()方法中需要測試某個條件時,為什麼用while,而不用if???直到看到了這個簡單的例子,終於明白了。。。。
這個例子是這樣的:
有兩個執行緒從List中刪除資料,而只有一個執行緒向List中新增資料。初始時,List為空,只有往List中添加了資料之後,才能刪除List中的資料。新增資料的執行緒向List新增完資料後,呼叫notifyAll(),喚醒了兩個刪除執行緒,但是它只添加了一個數據,而現在有兩個喚醒的刪除執行緒,這時怎麼辦??
如果用 if 測試List中的資料的個數,則會出現IndexOutofBoundException,越界異常。原因是,List中只有一個數據,第一個刪除執行緒把資料刪除後,第二個執行緒再去執行刪除操作時,刪除失敗,從而丟擲 IndexOutofBoundException。
但是如果用while 測試List中資料的個數,則不會出現越界異常!!!神奇。
當wait等待的條件發生變化時,會造成程式的邏輯混亂---即,List中沒有資料了,再還是有執行緒去執行刪除資料的操作。因此,需要用while迴圈來判斷條件的變化,而不是用if。
示例如下:Add類,負責新增資料:
public class Add { private String lock; public Add(String lock) { super(); this.lock = lock; } public void add() { synchronized (lock) { ValueObject.list.add("anyString"); lock.notifyAll(); } } } public class ThreadAdd extends Thread { private Add p; public ThreadAdd(Add p) { super(); this.p = p; } @Override public void run() { p.add(); } }
Subtract類,負責刪除資料----先要進行條件判斷,然後執行wait(),這意味著:wait等待的條件可能發生變化!!!
public class Subtract { private String lock; public Subtract(String lock) { super(); this.lock = lock; } public void subtract() { try { synchronized (lock) { if(ValueObject.list.size() == 0) {//將這裡的if改成while即可保證不出現越界異常!!!! System.out.println("wait begin ThreadName=" + Thread.currentThread().getName()); lock.wait(); System.out.println("wait end ThreadName=" + Thread.currentThread().getName()); } ValueObject.list.remove(0); System.out.println("list size=" + ValueObject.list.size()); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class ThreadSubtract extends Thread { private Subtract r; public ThreadSubtract(Subtract r) { super(); this.r = r; } @Override public void run() { r.subtract(); } }
封裝的List佇列:
public class ValueObject { public static List list = new ArrayList(); }
測試類:
public class Run { public static void main(String[] args) throws InterruptedException { String lock = new String(""); Add add = new Add(lock); Subtract subtract = new Subtract(lock); ThreadSubtract subtract1Thread = new ThreadSubtract(subtract); subtract1Thread.setName("subtract1Thread"); subtract1Thread.start(); ThreadSubtract subtract2Thread = new ThreadSubtract(subtract); subtract2Thread.setName("subtract2Thread"); subtract2Thread.start(); Thread.sleep(1000); ThreadAdd addThread = new ThreadAdd(add); addThread.setName("addThread"); addThread.start(); } }
參考:《JAVA多執行緒核心技術》