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。看来以后写代码也不能光图好看了,也要注意隐含的陷阱。

try...catch...finally的陷阱——加锁的线程开发经验分享

上一篇:SharePoint 2013 中使用 delegate control AdditionalPageHead 的注意事项


下一篇:VS2005 Web应用程序打包并安装数据库【转】