Java開發筆記(一百)線程同步synchronized

多個線程一塊兒辦事當然可以加快處理速度,可是也帶來一個問題:兩個線程同時爭搶某個資源時該怎麼辦?看來資源共享的另外一面即是資源衝突,正所謂魚與熊掌不可兼得,系統豈能讓多線程這項技術專佔好處?果真是有利必有弊,且看以前演示售票任務時候的多線程操做,具體代碼以下所示:html

	// 多個線程同時操做某個資源,可能會產生衝突
	private static void testConflict() {
		// 建立一個售票任務
		Runnable seller = new Runnable() {
			private Integer ticketCount = 100; // 可出售的車票數量
			
			@Override
			public void run() {
				while (ticketCount > 0) { // 還有餘票可供出售
					ticketCount--; // 餘票數量減一
					// 如下打印售票日誌,包括售票時間、售票線程、當前餘票等信息
					// 爲更好地重現資源衝突狀況,下面儘可能拉大訪問ticketCount的時間間隔
					SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
					String dateTime = sdf.format(new Date());
					String desc = String.format("%s %s 當前餘票爲%d張", dateTime, 
							Thread.currentThread().getName(), ticketCount);
					System.out.println(desc);
				}
			}
		};
		new Thread(seller, "售票線程A").start(); // 啓動售票線程A
		new Thread(seller, "售票線程B").start(); // 啓動售票線程B
		new Thread(seller, "售票線程C").start(); // 啓動售票線程C
	}

 

光光看代碼感受並沒有不妥之處,僅僅是起了三個售票線程共同賣票唄,這能有什麼問題?!假若只運行一次售票代碼,倒也看不出什麼名堂,但是一旦反覆地屢次運行這段售票代碼,那麼總會出現相似下列日誌的意外狀況,特別是在系統資源比較繁忙的時刻:多線程

10:56:38.182 售票線程A 當前餘票爲97張
10:56:38.182 售票線程B 當前餘票爲97張
10:56:38.182 售票線程C 當前餘票爲97張
10:56:38.186 售票線程B 當前餘票爲95張
10:56:38.186 售票線程A 當前餘票爲95張
10:56:38.186 售票線程C 當前餘票爲93張
………………………這裏省略餘下的日誌……………………

 

個人天,售票日誌居然打印出了相同的餘票數量,這正是多線程併發形成的結果。由於在ticketCount的自減語句和後面的日誌打印語句中間還有其它代碼,每行代碼都須要消耗一點點的時間,哪怕是零點幾毫秒,但就在這一瞬間,餘票可能又被別的線程賣掉了一張,因此等到線程A打印餘票日誌之時,ticketCount早已被賣了不止一次。如此一來,日誌打印先後的餘票數量遇到不一致的狀況,也就不足爲奇了。
問題的癥結在於餘票變量ticketCount是動態變化着的,三個售票線程爭先恐後地賣票,故而任一時刻的餘票數量均可能發生改變。解決問題的要點天然落在餘票的管控上面,正好Java提供了一個名叫synchronized的關鍵字,它可用來修飾某個方法或者某塊代碼,目的是限定該方法/代碼塊爲同步方法/同步代碼塊,也就是規定同一時刻只能有一個線程執行同步方法,其它線程來了之後必須在旁邊等待,直到先來的線程跑完同步方法,其它線程方可依次排隊執行該同步方法。
回到以前的售票代碼,第一反應是可否把售票任務的run方法設置爲同步方法?與其瞎猜想,不如試試再說,因而給run方法加上關鍵字synchronized以後的代碼片斷以下所示:併發

			// 指定整個run方法爲同步方法,這樣同一時刻只容許一個線程執行該方法
			public synchronized void run() {
				while (ticketCount > 0) { // 還有餘票可供出售
					ticketCount--; // 餘票數量減一
					// 如下打印售票日誌,包括售票時間、售票線程、當前餘票等信息
					String left = String.format("當前餘票爲%d張", ticketCount);
					PrintUtils.print(Thread.currentThread().getName(), left);
				}
			}

 

添加完畢再次運行售票代碼,觀察到了如下的售票日誌:ide

22:46:06.733 售票線程A 當前餘票爲99張
22:46:06.734 售票線程A 當前餘票爲98張
22:46:06.735 售票線程A 當前餘票爲97張
22:46:06.735 售票線程A 當前餘票爲96張
………………………這裏省略餘下的日誌……………………

 

可見如今只剩線程A在兀自賣票,而線程B和線程C呆在一旁陪太子讀書。原來synchronized給整個run方法加鎖,那麼只要線程A還沒有結束運行,線程B和線程C就都不容許置身其中,結果便退化爲只有一個線程在售票了。顯然給run方法添加synchronized的作法管得太多了,其實僅有ticketCount這個餘票變量會引發資源衝突,所以不妨縮小synchronized的管轄面,單單把餘票減一的代碼經過synchronized加以限定,並定義一個局部變量count來保存減一後的餘票數值。從新修改後的售票代碼片斷示例以下:優化

			public void run() {
				while (ticketCount > 0) { // 還有餘票可供出售
					int count;
					// 指定某個代碼塊爲同步代碼塊,這樣同一時刻只容許一個線程執行該段代碼
					synchronized (this) {
						count = --ticketCount; // 餘票數量減一
					}
					// 如下打印售票日誌,包括售票時間、售票線程、當前餘票等信息
					String left = String.format("當前餘票爲%d張", count);
					PrintUtils.print(Thread.currentThread().getName(), left);
				}
			}

 

屢次運行修改後的售票代碼,觀察到的售票日誌終於正常打印餘票數量了:this

16:33:10.265 售票線程A 當前餘票爲99張
16:33:10.265 售票線程C 當前餘票爲97張
16:33:10.265 售票線程B 當前餘票爲98張
16:33:10.266 售票線程A 當前餘票爲96張
16:33:10.266 售票線程B 當前餘票爲94張
16:33:10.266 售票線程C 當前餘票爲95張
………………………這裏省略餘下的日誌……………………

 

注意到上述的同步代碼塊把餘票數量賦值給一個局部變量,彷彿某個帶返回值的方法,既然這塊代碼的形式與方法相像,乾脆提取出來做爲獨立的同步方法,因而優化後的售票代碼變成了下面這般:線程

	// 把操做共享資源的代碼單獨提取出來做爲同步方法
	private static void testSyncMinMethod() {
		// 建立一個售票任務
		Runnable seller = new Runnable() {
			private Integer ticketCount = 100; // 可出售的車票數量
			
			@Override
			public void run() {
				while (ticketCount > 0) { // 還有餘票可供出售
					// 得到減一後的餘票數量。注意getDecreaseCount是個同步方法
					int count = getDecreaseCount();
					// 如下打印售票日誌,包括售票時間、售票線程、當前餘票等信息
					String left = String.format("當前餘票爲%d張", count);
					PrintUtils.print(Thread.currentThread().getName(), left);
				}
			}
			
			// 將餘票數量減一,並返回減後的餘票數量
			private synchronized int getDecreaseCount() {
				return --ticketCount; // 餘票數量減一
			}
		};
		new Thread(seller, "售票線程A").start(); // 啓動售票線程A
		new Thread(seller, "售票線程B").start(); // 啓動售票線程B
		new Thread(seller, "售票線程C").start(); // 啓動售票線程C
	}

 

以上代碼一樣有效避免了售票之時的資源衝突,而且代碼的組織結構更加清晰明瞭。日誌



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

相關文章
相關標籤/搜索