ThreadLocal详解,附带实例(threadlocal实现银行转账事务管理)

一.前言
  在很早之前接触到ThreadLocal很不了解一件事情,就是线程用来处理多线程情景,那为什么要用threadlocal来再为每个线程分发一个单独的变量副本,是否违背多线程的实际存在意义,而且threadlocal是否能用同步代替?
  其实还是有很大差别的,同步和锁解决问题最大的特点就是串行,虽然解决了问题,但是这样效率大大降低;相比之下,threadlocal可以并行,通过为每个线程分发单独的副本变量,来提高效率。两者都可以避免线程不安全的问题。用网上很好理解的一句话来说:同步锁是以时间换空间方式,而threadlocal是以空间换时间。各有各自的优缺点。
  
二.Threadlocal存储形式
  其实ThreadLocal实际上就是一个以当前线程为主键key,用户存入的变量副本为Value的Map集合,从源码我们可以看到:
  get方法
  从ThreadLocal的get()方法源码中,我们可以清晰看到主键就是Thread类型的t,而这个t正是当前线程。
  set方法
  从ThreadLocal的set()方法我们可以看到,我们通过主键t,将我们所要存储的变量副本存入这个ThreadLocalMap集合中。
  
三.运用ThreadLocal实现数据库事务管理,实现银行转账

1. 项目前言
  大家思考一下,转账的过程,假如A给B转账,需要两个过程,即一A先减钱,二B再加钱,大家思考这种情况,如果A减钱数据库操作没有问题,但是B在加钱操作数据库过程中出现异常,则造成A钱少了,B钱没有改变。这在转账过程中是绝对不允许出现的情况。所以这里要给数据库操作添加事务管理,让减钱和加钱融为一体要么都操作要么都不操作。
  可能有人会说将两个数据库操作写在一个方法里,直接用数据库事务不就ok了?是可以这只是两个sql语句操作,那多个数据库操作呢?这么做还符合MVC设计模式,符合代码解耦性么?答案当然是否定。
  所以我们可以运用ThreadLocal来为一个线程保存一个数据库Connection连接,这样不论多少数据库操作,只要运用的是一个Connection,就可以增加事务管理,这样极大的方便了我们想要实现的功能,而且不违背设计思想。
  
2. 代码实现
  首先说明,这里通过MVC设计模式设计尽可能做到解耦,中间有很多工具类,所以只给出重要部分代码,如有不清楚明白可以看另一篇博客(关于Druid数据库连接池实现Dao)

https://blog.csdn.net/a754895/article/details/82557395

2.1DataSourc工具类(重点)

package com.qjl.utils;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import com.alibaba.druid.pool.DruidDataSourceFactory;

/**
 * DataSource工具类
 * @author Joe
 *
 */
public class DataSourceUtils {
	
	//线程局部变量(map集合,key为thrad,value为connection)
	private static ThreadLocal<Connection> threadLocal;
	
	private static DataSource ds;
	
	static {
		threadLocal = new ThreadLocal<>();
		InputStream is = DataSourceUtils.class.getClassLoader().getResourceAsStream("database.properties");
		Properties properties = new Properties();
		try {
			properties.load(is);
			ds=DruidDataSourceFactory.createDataSource(properties);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public static DataSource getDataSource() {
		return ds;
	}
	
	public static Connection getConnection() throws Exception{
		//先从集合取
		Connection connection = threadLocal.get();
			if(connection == null) {
				connection = ds.getConnection();
				threadLocal.set(connection);
			}
		
		return connection;
	}
	
	/**
	 * 开启事务(start transcation)
	 */
	public static void beginTranscation() throws Exception{
		Connection connection = getConnection();
		connection.setAutoCommit(false);
	}
	
	/**
	 * 提交事务
	 * @throws Exception
	 */
	public static void commit() throws Exception{
		Connection connection = getConnection();
		connection.commit();
	}

	/**
	 * 回滚
	 * @throws Exception
	 */
	public static void rollback() throws Exception{
		Connection connection = getConnection();
		connection.rollback();
	}
	
	/**
	 * close
	 * @throws Exception
	 */
	public static void close() throws Exception{
		Connection connection = getConnection();
		threadLocal.remove(); //key就是当前线程,从当前线程解绑
		connection.close();
	}
}

这里的DataSource也就是我们的数据库连接工具类,通过getConnection()方法大家可以看到我们在这里向ThreadLocal存入了一个Connection,可能有人会有疑惑,不是说他是map集合吗?为什么之存储一个value呢?那key呢?当然,key就是我们当前线程,这在ThreadLocal内部已经写好了所以不用我们存入了。
  
2.2 AccountDaoImpl 接口实现类(数据库操作)

package com.qjl.dao.impl;

import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;

import com.qjl.dao.AccountDao;
import com.qjl.domain.Account;
import com.qjl.utils.DataSourceUtils;

public class AccountDaoImpl implements AccountDao{

	QueryRunner qr = new QueryRunner();
	
	@Override
	public void update(Account account) {
		try {
			qr.update(DataSourceUtils.getConnection(),"update account set money=? where id=?",account.getMoney(),account.getId());
		} catch (Exception e) {
			throw new RuntimeException("更新账户失败",e);
		}
	}

	@Override
	public Account findById(int id) {
		Account account = null;
		try {
			account = qr.query(DataSourceUtils.getConnection(),"select * from account where id=?",new BeanHandler<>(Account.class),id);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return account;
	}

}

在这里我们可以看到,在调用QueryRunner 的update或query方法执行sql语句的时候,我们同时传入了DataSourceUtils.getConnection(),这个其实就是我们刚刚给上面代码所编写的Connection,通过ThreadLocal让这个Connection是唯一,这样不论多少个数据库操作,这样就都用的是一个Connection了,这里说明一下(AB转账可以看成一个线程,CD转账是另一个线程,这样AB转账过程用的是一个Connection,CD转账过程用的则是另一个Connection,这样也就形成了我们多线程的实际例子)如图(自认为图比较清晰啦):
图

2.3 AccountServiceImpl服务层实现类

package com.qjl.service.impl;

import com.qjl.dao.AccountDao;
import com.qjl.dao.impl.AccountDaoImpl;
import com.qjl.domain.Account;
import com.qjl.service.AccountService;
import com.qjl.utils.DataSourceUtils;

public class AccountServiceImpl implements AccountService {

	@Override
	public void transMoney(int fromid, int toid, double money) {
		AccountDao accountDao = new AccountDaoImpl();
		try {
			// 0开启事务
			DataSourceUtils.beginTranscation();

			// 1查询用户
			Account from = accountDao.findById(fromid);
			Account to = accountDao.findById(toid);
			
			// 2减钱
			if (from == null && to == null) {
				throw new RuntimeException("账户不存在");
			}
			if (money > from.getMoney()) {
				throw new RuntimeException("余额不足");
			}
			from.setMoney(from.getMoney() - money);
			accountDao.update(from);
			
			// 3加钱
			to.setMoney(to.getMoney() + money);
			accountDao.update(to);
			
			// 4提交或者回滚
			DataSourceUtils.commit();
		} catch (Exception e) {
			try {
				DataSourceUtils.rollback();
				DataSourceUtils.commit();
			} catch (Exception e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
			throw new RuntimeException(e.getMessage());
		} finally {
			try {
				DataSourceUtils.close();
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

}

在这里我们先在开始时调用DataSourceUtils工具类中的beginTranscation()方法来开启事务,如果出现异常代码进入catch中,所以我们在catch中调用DataSourceUtils.rollback();和DataSourceUtils.commit();方法来实现回滚(也就是回到事务开启时的状态),如果没有异常才commit()提交事务,这样就做到了要么没有问题直接转账,要么不论转账过程中哪里出现异常,都会回滚,防止造成钱的丢失或异常增多。

其他代码就不给啦,这些就够了。

四.总结
  ThreadLocal大家可以就把他当作一个特殊的Map集合,key是当前线程,value是我们所需要的保存的变量,在多线程情况下,让不同的线程操作不同的变量副本,这样也就达成了我们想要线程安全的问题,同时并发也提高多线程的执行效率,当然ThreadLocal是不可以取代同步锁的,因为ThreadLocal还是有很大的局限性的,所以大家在使用时候一定要注意哦。

猜你喜欢

转载自blog.csdn.net/a754895/article/details/82747807