本文出處: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。看來以後寫代碼也不能光圖好看了,也要注意隱含的陷阱。