記錄一次生產環境的多線程併發問題

1.當時的場景

開一個線程,定時check hbase,避免hbase異常時,阻塞大量請求。

2.程序僞代碼

僞代碼如下:
定時用的是jdk自帶的工具類 Timer

具體邏輯:

private ExecutorService executorService = Executors.newSingleThreadExecutor();
public void run(){
	Future<Boolean> future = null;
	List<CheckResult> checkList = new Arraylist();
	while(){//避免由於網絡問題導致check不準確,所以失敗時會重試三次
		try{
			// CheckThread裏面是具體的check邏輯,會將check的結果放到checkList中
			// 爲了防止check超時,所以採用線程池的submit方法,返回一個future
			future = executorService.submit(new CheckThread(checkList));
			Boolean result = future.get(30,TimeUnit.SECONDS);
		}catch (Exception e){
			if(future != null){
				// 有異常時 中斷CheckThread執行
				future.cancel(true);				
			}
		}
	}
	// 增強for循環 遍歷 CheckList,得到最終的check結果緩存
	setCheckResult(checkList);	
}

到這,不知道你們能不能看出來併發問題?

3.導致的問題

將程序上線到prd的問題:Timer停止運行,hbase某個RegionServer恢復正常後,由於check停止了,沒有恢復RegionServer的狀態,導致了部分請求到該RegionServer的請求異常。查日誌,併發問題出在checkList上

4.併發問題

CheckThread是由線程池中的線程執行,在執行異常時雖然調用了future.cancel(true);但是這個方法只是給打上了中斷標記,並不能保證一定能中斷線程執行。也就是說當此線程在往checkList增加內容的同時;
setCheckResult(checkList)是由主線程執行,這個方法正在遍歷checkList:

for(CheckResult result : checkList){
}

增強for循環是採用的迭代器遍歷,也就是說在遍歷過程中如果list中的元素個數有變化就會報錯(fast-fail)。

一個線程在添加內容,一個線程在用增強for循環遍歷,最終也就導致了併發問題。

5.解決方案

知道原因,也就很好找解決方案了。
以上主要有兩個問題,一個是Timer停止問題,一個是併發問題,當然是因爲併發問題導致的Timer停止
其實在我們這個場景下,只要Timer不停止,就算出現併發問題,影響也不大,因爲check定時10秒就會執行一次,後面check結果就正常了,畢竟併發問題出現的概率不高。

解決辦法:
1.將run()方法的所有方法體給try catch,不要往外拋異常(Timer執行過程中有異常就會停止執行)
2.最簡單的方式,也是我採用的方式:重新new一個List對象:

//這個方法最終採用的是native方法:System.arraycopy();
List<CheckResult> list = new ArrayList(checkList);

到這就完了,這也更加警示了自己,只要涉及到多線程,就一定要小心謹慎,多多思考。因爲這種問題一般測試時測不出來的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章