try...catch...finally的陷阱——加鎖的執行緒開發經驗分享
阿新 • • 發佈:2019-02-19
本文出處: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。看來以後寫程式碼也不能光圖好看了,也要注意隱含的陷阱。