真实项目中 ThreadLocal 的妙用

本文介绍了ThreadLocal,它能提供线程局部变量,实现线程间数据隔离。还阐述了其在数据库连接池、HTTP Cookie等熟悉场景中的应用,保证每个线程的相关对象唯一。最后给出实战场景,表明它可让同一线程上下文间数据共享,使用时记得清理。

Java虫手 2019-07-07 14:53:57

真实项目中 ThreadLocal 的妙用

 

一、什么是 ThreadLocal

ThreadLocal 提供了线程的局部变量,每个线程都可以通过 set() 和 get() 来对这个局部变量进行操作,但不会和其他线程的局部变量冲突,实现了线程间的据隔离。

简单讲:一个获取用户的请求线程 A,如果向 ThreadLocal 填充变量 AValue(只能被线程 A 操作),该变量对其他获取用户的请求线程 B、C...是隔离的.

最简单的使用方式:

类似一次 HTTP 请求线程中,利用 ThreadLocal 存储 Cookie 对象,进行状态管理。set Cookie:

private ThreadLocal httpThreadLocal = new ThreadLocal();
httpThreadLocal.set("Cookie: sid=13420771402233")

上面存储格式是 String ,实际场景存储的是具体的对象。在这次 HTTP 请求过程中,任何时候都可以获取 Cookie 。获取方式很简单 get Cookie:

String cookieValue = (String) httpThreadLocal.get();

二、你熟悉的场景

2.1 数据库连接池

比如一次请求线程进来,业务 Dao 需要更新 user 表和 user-detail 表。如果是 new 出两个数据库 Connection ,分别不同的 Connection 操作 user 表和 user-detail 表,就无法保证事务。那么数据库连接池是如何保证的?

答案是:利用 ThreadLocal 存储唯一 Connection 对象。每次请求线程,pool.getConnection 获取连接的时候都会这样操作:

  • 会从 ThreadLocal 获取 Connection 对象。如果有,则保证了后面多个数据库操作共用同一个 Connection ,从而保证了事务。
  • 如果没有,往 ThreadLocal 新增Connection 对象,并返回到线程

错误的做法

public class XXXService {
 private Connection conn;
}

因为 conn 是线程不安全的。这样会导致多个请求共用一个连接。请求量很大的情况下,延迟各种。你懂。

因此,使用 ThreadLocal 保证每个请求线程的 Connection 是唯一的。即每个线程有自己的连接。

继续讲到 Spring 框架,在事务开始时,会给当前线程一个JdbcConnection, 在整个事务过程,都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离

2.2 HTTP Cookie

比如你访问百度、我访问百度,会有不同 Cookie 。而且你不能访问我的 Cookie,我也不能。顾名思义,使用 ThreadLocal 保证每个 HTTP 请求线程的 Cookie 是唯一的。

Cookie 这样才能做 Session 等状态管理。

三、实战场景

总结一下就是:ThreadLocal 可以让同一个线程中上下文之间数据共享

在上面章节 二、你熟悉的场景其实介绍了很多现有场景。那么我这边具体的实战场景是什么?

简单的例子:

适用满足这两个条件的场景:1.每个线程独有的一些信息,2.这些信息又会在多个方法或类中用到。

  1. 一个请求线程,里面有两个异步小线程,各有一个方法。分别处理 A 或 B 业务
  2. 一种方法是传递不可变的入参
  3. 另一种就是 ThreadLocal,放在 ThreadLocal 的入参,会被各个方法共享。而且多个请求线程互不影响

复杂的例子:

一次发货操作:会根据入参,进行组件化、流程编排化。那么入参会被各个地方用到,而且有些流程组件是异步的(类似 new thread 操作的)。这时候可以定一个 XXContext 上下文:

public class XXContext {
 
 private static ThreadLocal<Map<Class<?>, Object>> context = new InheritableThreadLocal<>();
 
 /**
 * 把参数设置到上下文的Map中
 */
 public static void put(Object obj) {
 Map<Class<?>, Object> map = context.get();
 if (map == null) {
 map = new HashMap<>();
 context.set(map);
 }
 if (obj instanceof Enum) {
 map.put(obj.getClass().getSuperclass(), obj);
 } else {
 map.put(obj.getClass(), obj);
 }
 }
 
 /**
 * 从上下文中,根据类名取出参数
 */
 @SuppressWarnings("unchecked")
 public static <T> T get(Class<T> c) {
 Map<Class<?>, Object> map = context.get();
 if (map == null) {
 return null;
 }
 return (T) map.get(c);
 }
 
 /**
 * 清空ThreadLocal的数据
 */
 public static void clean() {
 context.remove();
 }
}

代码解析:

  • 都是 static 操作,类似 DateUtil 玩法
  • 记得每次请求线程后清理。可以 AOP 去清理,加个注解就行。因为同一个请求线程可能被业务方公用。

关注、转发、评论头条号每天分享java 知识,

私信回复“源码”赠送Spring源码分析、Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式资料

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值