共计 2187 个字符,预计需要花费 6 分钟才能阅读完成。
一、什么是 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();
Thread 与 ThreadLocal 对象引用关系图
二、你熟悉的场景
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 框架,在事务开始时,会给当前线程一个 Jdbc Connection, 在整个事务过程,都是使用该线程绑定的 connection 来执行数据库操作,实现了事务的隔离性。Spring 框架里面就是用的 ThreadLocal 来实现这种隔离
2.2 HTTP Cookie
比如你访问百度、我访问百度,会有不同 Cookie。而且你不能访问我的 Cookie,我也不能。顾名思义,使用 ThreadLocal 保证每个 HTTP 请求线程的 Cookie 是唯一的。
Cookie 这样才能做 Session 等状态管理。
三、实战场景
总结一下就是:ThreadLocal 可以让同一个线程中上下文之间数据共享
在上面章节 二、你熟悉的场景 其实介绍了很多现有场景。那么我这边具体的实战场景是什么?
简单的例子:
适用满足这两个条件的场景:1. 每个线程独有的一些信息,2. 这些信息又会在多个方法或类中用到。
- 一个请求线程,里面有两个异步小线程,各有一个方法。分别处理 A 或 B 业务
- 一种方法是传递不可变的入参
- 另一种就是 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 去清理,加个注解就行。因为同一个请求线程可能被业务方公用。
(完)