try...catch...finally的陷阱——加鎖的線程開發經驗分享

本文出處:http://blog.csdn.net/chaijunkun/article/details/18318843,轉載請註明。由於本人不定期會整理相關博文,會對相應內容作出完善。因此強烈建議在原始出處查看此文。

 

最近在忙一些數據處理的項目。爲了方便操作,想把處理程序寫成HTTP接口。需要的時候在瀏覽器裏敲一下URL就可以執行相應功能。但是因爲一個業務往往需要處理幾個小時,等待HTTP返回是不現實的。於是把相關操作寫成了一個線程,URL調用後異步處理。數據是按天操作的,而HTTP接口調用了之後因爲網絡狀況不穩定可能會進行重試,如果對業務線程不加鎖進行限制,多次調用接口會產生多個業務線程,造成各種問題。於是我建立了下面的模型,同時也遇到了一個關於try...catch...finally的陷阱,下面就跟大家分享一下。

 

首先創建一個存儲任務名稱和當前狀態的HashMap,然後再建立插入、刪除和查詢當前任務的方法。由於是多線程操作的,需要在方法內對HashMap進行同步,代碼如下:

 

package net.csdn.blog.chaijunkun.thread;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.apache.log4j.Logger;

import net.csdn.blog.chaijunkun.util.ObjectUtil;

public class ThreadLocker {
	
	private static final Logger logger= Logger.getLogger(ThreadLocker.class); 
	
	private static final Map<String, String> taskmap= new HashMap<String, String>();
	
	/**
	 * 提交任務
	 * @param taskName 任務名稱
	 * @param status 任務狀態
	 * @return 發現重名提交失敗 返回false 否則爲true
	 */
	public static boolean submitTask(String taskName, String status){
		synchronized (taskmap) {
			if (taskmap.containsKey(taskName)){
				return false;
			}else{
				taskmap.put(taskName, status);
				return true;
			}
		}
	}
	
	/**
	 * 更新任務狀態
	 * @param taskName 任務名稱
	 * @param status 任務狀態
	 * @return 無指定任務返回false 否則更新狀態後返回true
	 */
	public static boolean updateTask(String taskName, String status){
		synchronized (taskmap) {
			if (taskmap.containsKey(taskName)){
				taskmap.put(taskName, status);
				return true;
			}else{
				return false;
			}
		}
	}
	
	/**
	 * 移除指定任務
	 * @param taskName 任務名稱
	 */
	public static void removeTask(String taskName){
		synchronized (taskmap) {
			if (taskName.contains(taskName)){
				taskmap.remove(taskName);
			}
		}
	}
	
	/**
	 * 列出當前正在執行的任務
	 * @return
	 */
	public static List<String> listTask(){
		synchronized (taskmap) {
			if (ObjectUtil.isNotEmpty(taskmap)){
				Set<Entry<String, String>> entrySet= taskmap.entrySet();
				List<String> retVal= new LinkedList<String>();
				for (Entry<String, String> entry : entrySet) {
					retVal.add(String.format("任務:%s, 狀態:%s", entry.getKey(), entry.getValue()));
				}
				return retVal;
			}else{
				return null;
			}
		}
	}
	
	public static void main(String[] args) {
		try {
			for(int i=0; i<10; i++){
				TestThread t= new TestThread(i);
				t.start();
			}
			List<String> taskList= ThreadLocker.listTask();
			if (ObjectUtil.isNotEmpty(taskList)){
				for (String taskInfo : taskList) {
					logger.info(taskInfo);
				}
			}
			Thread.sleep(10000L);
		} catch (InterruptedException e) {
			logger.error(e);
		}
	}

}


任務名稱在我的真實代碼中採用“業務名稱+日期”,本文中我採用固定的名稱“lock_thread”,因此在上述DemoCode應該只能啓動一個線程來處理業務。下面貼出業務線程的寫法:

 

 

package net.csdn.blog.chaijunkun.thread;

import org.apache.log4j.Logger;

public class TestThread extends Thread {
	
	private static final Logger logger= Logger.getLogger(TestThread.class);	
	
	private int id;
	
	private String getTaskName(){
		return "lock_thread";
	}
	
	public TestThread(int id){
		this.id= id;
		this.setName(this.getTaskName());
	}
	
	public void run(){
		String taskName= this.getName();
		try{
			//上鎖
			if (!ThreadLocker.submitTask(taskName, String.format("示例線程, id:%d", this.id))){
				//logger.info(String.format("[id:%s][加鎖失敗]", this.id));
				return;
			}else{
				//logger.info(String.format("[id:%s][加鎖成功]", this.id));
			}
			//線程要做的事情
			for(int i=0; i<20; i++){
				logger.info(String.format("[id:%s][print:%d]", this.id, i));
				Thread.sleep(1L);
			}
		} catch (Exception e) {
			logger.error(e);
		} finally{
			//解鎖
			//logger.info(String.format("[id:%s][銷燬]", this.id));
			ThreadLocker.removeTask(taskName);
		}
	}

}


上述線程代碼中,開始爲了代碼統一,我把上鎖的代碼放在了try中,之所以要採用try...catch...finally的寫法是因爲在業務處理過程中有多種錯誤發生不允許繼續執行,因此我希望不管發生什麼錯誤,最終都應該把鎖解開。

 

 

好了,願望是美好的,看看執行結果吧:

 

2014-01-15 19:29:22,538 INFO [lock_thread] - TestThread.run(32) | [id:6][print:0]
2014-01-15 19:29:22,538 INFO [lock_thread] - TestThread.run(32) | [id:8][print:0]
2014-01-15 19:29:22,538 INFO [lock_thread] - TestThread.run(32) | [id:0][print:0]
2014-01-15 19:29:22,538 INFO [lock_thread] - TestThread.run(32) | [id:1][print:0]
2014-01-15 19:29:22,538 INFO [lock_thread] - TestThread.run(32) | [id:5][print:0]
2014-01-15 19:29:22,585 INFO [lock_thread] - TestThread.run(32) | [id:5][print:1]
2014-01-15 19:29:22,586 INFO [lock_thread] - TestThread.run(32) | [id:1][print:1]
2014-01-15 19:29:22,586 INFO [lock_thread] - TestThread.run(32) | [id:8][print:1]
2014-01-15 19:29:22,586 INFO [lock_thread] - TestThread.run(32) | [id:0][print:1]
2014-01-15 19:29:22,586 INFO [lock_thread] - TestThread.run(32) | [id:6][print:1]
2014-01-15 19:29:22,587 INFO [lock_thread] - TestThread.run(32) | [id:1][print:2]
2014-01-15 19:29:22,587 INFO [lock_thread] - TestThread.run(32) | [id:5][print:2]
2014-01-15 19:29:22,595 INFO [lock_thread] - TestThread.run(32) | [id:0][print:2]
2014-01-15 19:29:22,595 INFO [lock_thread] - TestThread.run(32) | [id:6][print:2]
2014-01-15 19:29:22,595 INFO [lock_thread] - TestThread.run(32) | [id:8][print:2]
2014-01-15 19:29:22,596 INFO [lock_thread] - TestThread.run(32) | [id:0][print:3]
2014-01-15 19:29:22,596 INFO [lock_thread] - TestThread.run(32) | [id:5][print:3]
.....


坑爹了,居然沒鎖住,線程全起來了。爲什麼會這樣!!!後來我把解鎖代碼放到了finally的外面,或者把加鎖代碼放到了try外面:

 

 

public void run(){
	String taskName= this.getName();
	//上鎖
	if (!ThreadLocker.submitTask(taskName, String.format("示例線程, id:%d", this.id))){
		//logger.info(String.format("[id:%s][加鎖失敗]", this.id));
		return;
	}else{
		//logger.info(String.format("[id:%s][加鎖成功]", this.id));
	}
	try{
		//線程要做的事情
		for(int i=0; i<20; i++){
			logger.info(String.format("[id:%s][print:%d]", this.id, i));
			Thread.sleep(1L);
		}
	} catch (Exception e) {
		logger.error(e);
	} finally{
		//解鎖
		//logger.info(String.format("[id:%s][銷燬]", this.id));
		ThreadLocker.removeTask(taskName);
	}
}


居然正常了:

 

 

2014-01-15 19:34:26,239 INFO [lock_thread] - TestThread.run(32) | [id:3][print:0]
2014-01-15 19:34:26,245 INFO [lock_thread] - TestThread.run(32) | [id:3][print:1]
2014-01-15 19:34:26,246 INFO [lock_thread] - TestThread.run(32) | [id:3][print:2]
2014-01-15 19:34:26,247 INFO [lock_thread] - TestThread.run(32) | [id:3][print:3]
2014-01-15 19:34:26,248 INFO [lock_thread] - TestThread.run(32) | [id:3][print:4]
2014-01-15 19:34:26,249 INFO [lock_thread] - TestThread.run(32) | [id:3][print:5]
2014-01-15 19:34:26,250 INFO [lock_thread] - TestThread.run(32) | [id:3][print:6]
2014-01-15 19:34:26,251 INFO [lock_thread] - TestThread.run(32) | [id:3][print:7]
2014-01-15 19:34:26,254 INFO [lock_thread] - TestThread.run(32) | [id:3][print:8]
2014-01-15 19:34:26,255 INFO [lock_thread] - TestThread.run(32) | [id:3][print:9]
2014-01-15 19:34:26,256 INFO [lock_thread] - TestThread.run(32) | [id:3][print:10]
2014-01-15 19:34:26,257 INFO [lock_thread] - TestThread.run(32) | [id:3][print:11]
2014-01-15 19:34:26,258 INFO [lock_thread] - TestThread.run(32) | [id:3][print:12]
2014-01-15 19:34:26,259 INFO [lock_thread] - TestThread.run(32) | [id:3][print:13]
2014-01-15 19:34:26,260 INFO [lock_thread] - TestThread.run(32) | [id:3][print:14]
2014-01-15 19:34:26,261 INFO [lock_thread] - TestThread.run(32) | [id:3][print:15]
2014-01-15 19:34:26,264 INFO [lock_thread] - TestThread.run(32) | [id:3][print:16]
2014-01-15 19:34:26,265 INFO [lock_thread] - TestThread.run(32) | [id:3][print:17]
2014-01-15 19:34:26,266 INFO [lock_thread] - TestThread.run(32) | [id:3][print:18]
2014-01-15 19:34:26,267 INFO [lock_thread] - TestThread.run(32) | [id:3][print:19]

 

 

不斷查找問題的根源。在開始的代碼中將TestThread的log解註釋後,執行貌似也正常了,但是這應該不是bug的根源。


後來仔細研究了一下try...catch...finally的執行邏輯。在try代碼塊中的return返回前會執行finally中的代碼塊,這誰都知道。但是由於一時糊塗,把加鎖代碼放在了try裏面,當發現重名任務無法提交時,線程本應該直接退出,並且不應該解鎖,但事實上return前也執行了finally中的解鎖邏輯,因此出現了看起來加鎖無效的bug。看來以後寫代碼也不能光圖好看了,也要注意隱含的陷阱。

發佈了68 篇原創文章 · 獲贊 252 · 訪問量 147萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章