1. 程式人生 > >try...catch...finally的陷阱——加鎖的執行緒開發經驗分享

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。看來以後寫程式碼也不能光圖好看了,也要注意隱含的陷阱。