Java多執行緒學習(四)等待/通知(wait/notify)機制

SnailClimb發表於2018-03-25

轉載請備註地址:https://juejin.im/post/5ab755fc6fb9a028c22aba1f

系列文章傳送門:

Java多執行緒學習(一)Java多執行緒入門

Java多執行緒學習(二)synchronized關鍵字(1)

Java多執行緒學習(二)synchronized關鍵字(2)

Java多執行緒學習(三)volatile關鍵字

Java多執行緒學習(四)等待/通知(wait/notify)機制

Java多執行緒學習(五)執行緒間通訊知識點補充

系列文章將被優先更新於微信公眾號“Java面試通關手冊”,歡迎廣大Java程式設計師和愛好技術的人員關注。

本節思維導圖:

本節思維導圖

思維導圖原始檔+思維導圖軟體關注微信公眾號:“Java面試通關手冊” 回覆關鍵字:“Java多執行緒” 免費領取。

一 等待/通知機制介紹

1.1 不使用等待/通知機制

當兩個執行緒之間存在生產和消費者關係,也就是說第一個執行緒(生產者)做相應的操作然後第二個執行緒(消費者)感知到了變化又進行相應的操作。比如像下面的whie語句一樣,假設這個value值就是第一個執行緒操作的結果,doSomething()是第二個執行緒要做的事,當滿足條件value=desire後才執行doSomething()。

但是這裡有個問題就是:第二個語句不停過通過輪詢機制來檢測判斷條件是否成立。如果輪詢時間的間隔太小會浪費CPU資源,輪詢時間的間隔太大,就可能取不到自己想要的資料。所以這裡就需要我們今天講到的等待/通知(wait/notify)機制來解決這兩個矛盾

    while(value=desire){
        doSomething();
    }
複製程式碼

1.2 什麼是等待/通知機制?

通俗來講:

等待/通知機制在我們生活中比比皆是,一個形象的例子就是廚師和服務員之間就存在等待/通知機制。

  1. 廚師做完一道菜的時間是不確定的,所以菜到服務員手中的時間是不確定的;
  2. 服務員就需要去“等待(wait)”;
  3. 廚師把菜做完之後,按一下鈴,這裡的按鈴就是“通知(nofity)”;
  4. 服務員聽到鈴聲之後就知道菜做好了,他可以去端菜了。

用專業術語講:

等待/通知機制,是指一個執行緒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"最後輸出可以看出,notify()執行後並不會立即釋放鎖。下面我們會補充介紹這個知識點。

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可以改變執行緒物件的狀態。如下圖所示:

執行緒的基本狀態切換圖

  1. 新建(new):新建立了一個執行緒物件。

  2. 可執行(runnable):執行緒物件建立後,其他執行緒(比如main執行緒)呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲 取cpu的使用權。

  3. 執行(running):可執行狀態(runnable)的執行緒獲得了cpu時間片(timeslice),執行程式程式碼。

  4. 阻塞(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)狀態。

  5. 死亡(dead):執行緒run()、main()方法執行結束,或者因異常退出了run()方法,則該執行緒結束生命週期。死亡的執行緒不可再次復生。

備註: 可以用早起坐地鐵來比喻這個過程:

還沒起床:sleeping

起床收拾好了,隨時可以坐地鐵出發:Runnable

等地鐵來:Waiting

地鐵來了,但要排隊上地鐵:I/O阻塞

上了地鐵,發現暫時沒座位:synchronized阻塞

地鐵上找到座位:Running

到達目的地:Dead

2.3 notify()鎖不釋放

當方法wait()被執行後,鎖自動被釋放,但執行玩notify()方法後,鎖不會自動釋放。必須執行完otify()方法所在的synchronized程式碼塊後才釋放。

下面我們通過程式碼驗證一下:

(完整程式碼:github.com/Snailclimb/…

帶wait方法的synchronized程式碼塊

			synchronized (lock) {
				System.out.println("begin wait() ThreadName="
						+ Thread.currentThread().getName());
				lock.wait();
				System.out.println("  end wait() ThreadName="
						+ Thread.currentThread().getName());
			}
複製程式碼

帶notify方法的synchronized程式碼塊

			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程式碼塊。

執行效果如下:

執行效果
這也驗證了我們剛開始的結論:必須執行完notify()方法所在的synchronized程式碼塊後才釋放。

2.4 當interrupt方法遇到wait方法

當執行緒呈wait狀態時,對執行緒物件呼叫interrupt方法會出現InterrupedException異常。

Service.java

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了!");
		}
	}
}
複製程式碼

ThreadA.java

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);
	}

}

複製程式碼

Test.java

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();
		}

	}

}
複製程式碼

執行結果:

執行結果

參考:

《Java多執行緒程式設計核心技術》

《Java併發程式設計的藝術》

相關文章