Java開發筆記(一百零三)線程間的通訊方式

前面介紹了多線程併發之時的資源搶佔狀況,以及利用同步、加鎖、信號量等機制解決資源衝突問題,不過這些機制只適合同一資源的共享分配,並未涉及到某件事由的來龍去脈。平常生活中,常常存在兩個先後關聯的事務,像僱員和僱主這兩個角色,他們之間的某些工做就帶有因果關係。好比要等僱主接到了項目,僱員纔有活幹;又如每個月末員工都等着老闆發工資,這樣纔有錢逛街和吃大餐,此時員工的消費行爲便依賴於老闆的發薪水動做。如此看來,兩個線程之間理應創建某種消息通路,每當線程A完成某個事項,就將完成標誌通知線程B,線程B收到通知以後,認爲前提條件已經知足,這才進行後續的處理過程。線程之間的消息通路,可視做在線程間傳遞信息,專業的說法叫作「通訊」,如何在多線程併發時進行有效通訊,這是多線程技術中的一大課題。
依據線程併發時的不一樣管理機制,線程間的通訊也各有不一樣的方式,接下來將分別論述同步機制與加鎖機制之下的兩種線程通訊過程。
首先是同步機制,採用同步代碼塊的話,須要在關鍵字synchronized後面補充待同步的對象實例,以前的同步代碼塊統一寫成「synchronized (this)」。但是圓括號內部必定要填this嗎?圓括號的內部參數到底是幹什麼用的?其實synchronized附帶的圓括號參數正是在線程間通訊的郵差,之前的同步演示代碼因爲沒進行線程通訊,所以圓括號裏的參數沒有具體要求,通常填this便可。如今要想在線程間進行通訊,就必須啓用圓括號參數了,而且兩個線程都要在synchronized後面填寫該參數對象。
舉個例子,僱員等着僱主發工資,那員工怎樣才知道老闆已經發了呢?要是由員工本身一下子一下子去查銀行卡,平時的工做都會受到影響,因此可以讓員工留個等工資的心眼就好。而後老闆一個一個發工資,發完以後給員工遞個工資條,或者給員工發封工資郵件,這樣員工收到工資條便知薪水到帳了。那麼在等工資和發工資這兩個線程之間,便可令工資條做爲兩者的信使,因而同步代碼塊可改寫爲「synchronized (工資條對象)」的形式。同時工資條對象還要支持等待與發放兩個動做,由於這類動做早就隱藏在Object類的基本方法中,因此開發者沒必要擔憂工資條對象該爲Integer類型仍是別的什麼類型,凡是正常的實例都擁有等待與發放的方法,具體的方法說明以下:
wait:等待通知。
notify:在等待隊列中隨機挑選一個線程發放通知。
notifyAll:向等待隊列中的全部線程發放通知。
在編碼實現同步機制的通訊過程時,先分別建立僱員和僱主的工做任務,其中僱員任務在同步代碼塊中調用工資條對象的wait方法,表示等着發工資;而僱主任務在同步代碼塊中調用工資條對象的notify方法,表示發完工資了。而後依次啓動員工線程和老闆線程,員工線程負責等工資以及收到工資後的消費行爲,老闆線程負責發工資以及記帳操做。據此編寫的同步線程通訊代碼示例以下:html

	// 員工與老闆之間經過工資條通訊
	private static Integer salary = 5000;
	
	// 測試經過wait和notify方法進行線程間通訊
	private static void testWaitNotify() {
		// 建立僱員的工做任務
		Runnable employee = new Runnable() {
			@Override
			public void run() {
				PrintUtils.print(Thread.currentThread().getName(), "等着發工資。");
				synchronized (salary) { // 工資是個人,大家別搶
					try {
						salary.wait(); // 等待發工資
						// 打印拿到工資後的慶祝日誌
						PrintUtils.print(Thread.currentThread().getName(), "今晚趕忙吃大餐。");
					} catch (InterruptedException e) { // 等待期間容許接收中斷信號
						e.printStackTrace();
					}
				}
			}
		};
		// 建立僱主的工做任務
		Runnable boss = new Runnable() {
			@Override
			public void run() {
				// 稍等一下子,老闆線程的同步代碼塊務必在員工線程的同步代碼塊以後開始運行,不然員工線程將一直等待
				wait_a_moment();
				PrintUtils.print(Thread.currentThread().getName(), "開始發工資。");
				synchronized (salary) { // 由我發工資,大家別鬧
					wait_a_moment(); // 銀行轉帳也須要時間
					salary.notify(); // 隨機通知其中一個等待線程
					// 手好酸,發工資也是個體力活,記個帳
					PrintUtils.print(Thread.currentThread().getName(), "發完工資了。");
				}
			}
		};
		new Thread(employee, "同步機制的員工").start(); // 啓動員工等工資的線程
		new Thread(boss, "同步機制的老闆").start(); // 啓動老闆發工資的線程
	}

	// 稍等一下子,模擬平常事務的時間消耗
	private static void wait_a_moment() {
		int delay = new Random().nextInt(500); // 生成500之內的隨機整數
		try {
			Thread.sleep(delay); // 睡眠若干毫秒
		} catch (InterruptedException e) {
		}
	}

運行上面的線程通訊代碼,打印出如下的線程日誌:多線程

14:37:29.685 同步機制的員工 等着發工資。
14:37:29.994 同步機制的老闆 開始發工資。
14:37:30.120 同步機制的老闆 發完工資了。
14:37:30.120 同步機制的員工 今晚趕忙吃大餐。

 

從日誌可見,員工線程果真在等到工資以後纔去吃大餐。併發

同步機制可以經過wait/notify完成線程通訊功能,那麼加鎖機制又該如何進行線程間通訊呢?既然加鎖機制設計了專門的鎖工具,那麼鎖鑰內外的線程也只能經過鎖工具來通訊,信使則爲調用鎖對象的newCondition方法返回的Condition條件對象。條件對象一樣擁有等待與發放的方法,且與Object類的三個方法一一對應,具體說明以下:
await:等待通知。
signal:在等待隊列中隨機挑選一個線程發放通知。
signalAll:向等待隊列中的全部線程發放通知。
以可重入鎖ReentrantLock爲例,依然要先分別建立僱員和僱主的工做任務,其中僱員任務在加鎖以後再調用條件對象的await方法,表示等着發工資;而僱主任務在加鎖以後再調用條件對象的signal方法,表示發完工資了;另外僱員任務和僱主任務均需在結束以前進行解鎖。而後依次啓動員工線程和老闆線程,員工線程負責等工資以及收到工資後的消費行爲,老闆線程負責發工資以及記帳操做。下面是在加解鎖線程之間進行通訊的代碼例子:dom

	// 建立一個可重入鎖
	private final static ReentrantLock reentrantLock = new ReentrantLock();
	// 獲取可重入鎖的條件對象
	private static Condition condition = reentrantLock.newCondition();
	
	// 測試經過Condition對象進行線程間通訊
	private static void testCondition() {
		// 建立僱員的工做任務
		Runnable employee = new Runnable() {
			@Override
			public void run() {
				PrintUtils.print(Thread.currentThread().getName(), "等着發工資。");
				reentrantLock.lock(); // 對可重入鎖加鎖
				try {
					condition.await(); // 這裏在等待條件對象的信號
					// 打印拿到工資後的慶祝日誌
					PrintUtils.print(Thread.currentThread().getName(), "今晚趕忙吃大餐。");
				} catch (InterruptedException e) { // 等待期間容許接收中斷信號
					e.printStackTrace();
				}
				reentrantLock.unlock(); // 對可重入鎖解鎖
			}
		};
		// 建立僱主的工做任務
		Runnable boss = new Runnable() {
			@Override
			public void run() {
				// 稍等一下子,老闆線程的加鎖務必在員工線程的加鎖以後執行,不然員工線程將一直等待
				wait_a_moment();
				PrintUtils.print(Thread.currentThread().getName(), "開始發工資。");
				reentrantLock.lock(); // 對可重入鎖加鎖
				wait_a_moment(); // 銀行轉帳也須要時間
				condition.signal(); // 給條件對象發送信號
				// 手好酸,發工資也是個體力活,記個帳
				PrintUtils.print(Thread.currentThread().getName(), "發完工資了。");
				reentrantLock.unlock(); // 對可重入鎖解鎖
			}
		};
		new Thread(employee, "加鎖機制的員工").start(); // 啓動員工等工資的線程
		new Thread(boss, "加鎖機制的老闆").start(); // 啓動老闆發工資的線程
	}

 

運行上述的線程通訊代碼,打印出以下的線程日誌:ide

14:57:07.794 加鎖機制的員工 等着發工資。
14:57:07.801 加鎖機制的老闆 開始發工資。
14:57:07.905 加鎖機制的老闆 發完工資了。
14:57:07.906 加鎖機制的員工 今晚趕忙吃大餐。

 

可見加鎖機制一樣實現了線程間通訊的功能。工具



更多Java技術文章參見《Java開發筆記(序)章節目錄測試

相關文章
相關標籤/搜索