ThreadLocal用法与问题
使用场景
线程绑定连接实现线程隔离
在数据库操作中,事务的控制通常依赖于连接。为了实现线程内的事务隔离,我们可以使用 Thread Local 为每个线程绑定一个连接。
String outUser = "zhangan";
String inUser = "lisi";
int money = 10086;
boolean result = accountService.transfer(outUser, inUser, money);
//转账
public boolean transfer(String outUser, String inUser, int money) {
Connection conn = null;
try {
//1. 开启事务
conn = jdbcUtils.getConnection();
conn.setAutoCommit(false);
// 转出
accountDao.out(outUser, money);
//算术异常: 模拟转出成功,转入失败
int i = 1 / 0;
// 转入
accountDao.in(inUser, money);
//2. 成功提交
jdbcUtils.commitAndClose(conn);
} catch (Exception e) {
e.printStackTrace();
//2. 或者失败回滚
jdbcUtils.rollbackAndClose(conn);
return false;
}
return true;
}
JDBCUtils:
我们使用 Thread Local 来存储每个线程的连接。在需要进行数据库操作时,首先从 Thread Local 中获取连接,如果没有则从连接池中获取一个连接并绑定到当前线程。这样可以确保每个线程都有自己独立的连接,从而更好地控制事务。
@Component
public class JdbcUtils {
static ThreadLocal<Connection> tl = new ThreadLocal<>();
@Autowired
private DruidDataSource dataSource;
// 获取连接
/*
* 原本: 直接从连接池中获取连接
* 现在:
* 1. 直接获取当前线程绑定的连接对象
* 2. 如果连接对象是空的
* 2.1 再去连接池中获取连接
* 2.2 将此连接对象跟当前线程进行绑定
* */
public Connection getConnection() throws SQLException {
Connection conn = tl.get();
if(conn == null){
conn = dataSource.getConnection();
tl.set(conn);
}
return conn;
}
//释放资源
public void release(AutoCloseable... ios){
for (AutoCloseable io : ios) {
if(io != null){
try {
io.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public void commitAndClose(Connection conn) {
try {
if(conn != null){
//提交事务
conn.commit();
//解绑当前线程绑定的连接对象
tl.remove();
//释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public void rollbackAndClose(Connection conn) {
try {
if(conn != null){
//回滚事务
conn.rollback();
//解绑当前线程绑定的连接对象
tl.remove();
//释放连接
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
线程内保存全局变量
在 Web 应用中,我们经常需要在拦截器中获取用户信息,并在后续的方法中使用。使用 Thread Local 可以避免参数传递的麻烦。
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<User> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取登录的用户信息
User attribute = (User) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null) {
//把登录后用户的信息放在ThreadLocal里面进行保存
loginUser.set(attribute);
return true;
} else {
//未登录,返回登录页面
return false;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
loginUser.remove();
}
}
代替参数传递
在方法调用的过程中,如果有一个参数需要在多个方法之间传递,并且中间有很多复杂的逻辑处理,直接使用参数传递会造成代码冗余。这时可以使用 Thread Local 来代替参数传递。
static final ThreadLocal<String> threadLocal = new ThreadLocal();
@Autowired
private AService aService;
public void dealTransfer() {
String address = "nanjing";
threadLocal.set(address);
A();
}
private void A() {
B();
}
private void B() {
aService.C();
}
们在方法 A 中设置了一个值到 ThreadLocal 中,然后在方法 B 中调用了 AService 的方法 C。在方法 C 中,可以直接从 Thread Local 中获取这个值,而不需要通过参数传递。
每个线程独享工具类对象
在多线程环境下,一些工具类可 能不是线程安全的。例如,SimpleDateFormat
在多线程环境下使用可能会出现问题。我们可以使用 ThreadLocal 来为每个线程提供一个独享的工具类对象,每个线程都有自己独立的对象副本,避免了线程不安全的问题。
public class ThreadSafeFormatterUtil {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
//创建一份 SimpleDateFormat 对象
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
for (int i = 0; i < 30; i++) {
new Thread(new Runnable() {
@Override
public void run() {
SimpleDateFormat simpleDateFormat = ThreadSafeFormatterUtil.dateFormatThreadLocal.get();
String date = simpleDateFormat.format(new Date());
System.out.println(date);
}
}).start();
}