Java多線程學習(四)等待/通知(wait/notify)機制

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

系列文章傳送門:git

Java多線程學習(一)Java多線程入門程序員

Java多線程學習(二)synchronized關鍵字(1)github

Java多線程學習(二)synchronized關鍵字(2)面試

Java多線程學習(三)volatile關鍵字編程

Java多線程學習(四)等待/通知(wait/notify)機制微信

Java多線程學習(五)線程間通訊知識點補充多線程

系列文章將被優先更新於微信公衆號「Java面試通關手冊」,歡迎廣大Java程序員和愛好技術的人員關注。併發

本節思惟導圖: ide

本節思惟導圖

思惟導圖源文件+思惟導圖軟件關注微信公衆號:「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併發編程的藝術》

相關文章
相關標籤/搜索