ThreadLocal
1. ThreadLocal 基本概念
ThreadLocal 提供了线程局部变量,这些变量与普通变量不同,每个访问该变量的线程都有自己独立的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,用于将状态(如用户 ID 或事务 ID)与线程关联起来。
- 线程隔离:每个线程都有自己独立的变量副本
- 无锁操作:不需要同步机制,因为每个线程操作自己的副本
- 生命周期:与线程绑定,线程结束时自动清理
2. Thread 和 ThreadLocal 关系
既然 ThreadLocal 是线程内部的局部变量,肯定和线程是有关系的,怎么扯商关系的?
Thread 里就维护了一个 ThreadLocal.ThreadLocalMap 类型的变量
这个变量没有修饰符,访问权限默认是包私有,Thread 和 ThreadLocal 是同一个包
// Thread 类
public class Thread implements Runnable {
// ThreadLocal 的底层存储结构
ThreadLocal.ThreadLocalMap threadLocals = null;
// InheritableThreadLocal 的底层存储结构
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
3. ThreadLocal 类结构
public class ThreadLocal<T> {
// 构造
public ThreadLocal() { }
// 匿名内部类方式设置初始值
protected T initialValue() { return null; }
// 也是设置初始值
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {}
// 获取值
public T get() { }
// 设置值
public void set(T value) { }
// 清空
public void remove() {}
// 内部类(就是 Thread.threadLocals,是一个 map)
static class ThreadLocalMap { }
}
initialValue()
和 withInitial()
方法都是用来设置初值的,对比下两者区别:
// initialValue 方式
ThreadLocal<String> threadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "默认值";
}
};
// withInitial 方式(JDK8提供的,这个方法更简洁是主流方式)
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认值");
内部类 ThreadLocalMap
是一个定制化的哈希表,内部维护了一个 Entry
类(存储键值对),是弱引用类型
static class ThreadLocalMap {
// 弱引用,后面细说
static class Entry extends WeakReference<ThreadLocal<?>> { }
}
4. ThreadLocal 实现原理
set() 方法实现
public void set(T value) {
Thread t = Thread.currentThread(); // 当前线程
ThreadLocalMap map = getMap(t); // 拿到线程的 threadLocals 变量(没修饰符,包私有)
if (map != null) // 如果 threadLocals 不为空,就往里添加
map.set(this, value); // this
else // 如果 threadLocals 为空就初始化
createMap(t, value);
}
当 threadLocals 为空就初始化,并把值添加进去,源码如下:
void createMap(Thread t, T firstValue) {
// 调用构造方法
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// 初始化逻辑,哈希表基本都是这一套流程
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 1. 初始化Entry数组(哈希表),初始容量为INITIAL_CAPACITY(默认16)
table = new Entry[INITIAL_CAPACITY];
// 2. 计算数组下标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 3. 在计算出的位置创建新的Entry节点,存储键值对
table[i] = new Entry(firstKey, firstValue);
// 4. 设置当前元素数量为1(因为只插入了第一个元素)
size = 1;
// 5. 设置扩容阈值(通常是容量的2/3)
setThreshold(INITIAL_CAPACITY);
}
当 threadLocals 不为空,就调用 map.set(this, value);
来添加值,需要先理解下这个 this
因为一个线程中可以有多个 ThreadLolcal 实例,所以 key 是 this
ThreadLocal<String> userTL = new ThreadLocal<>();
ThreadLocal<Integer> ageTL = new ThreadLocal<>();
// 同一线程内存储两个独立变量
userTL.set("Alice"); // this是userTL
ageTL.set(25); // this是ageTL
再来看 map.set(this, value)
原理,源码如下:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table; // ThreadLocalMap 的 数组
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); // 计算下标
// 源码是:Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]
// 转成 while 循环,效果一致
Entry e = tab[i]; // 1. 从初始位置开始检查,获取当前位置的 Entry
while (e != null) { // 2. e != null,说明 i 下标位置有元素
ThreadLocal<?> k = e.get(); // 检查当前Entry的key
if (k == key) { // 情况1:找到相同的key,直接覆盖
e.value = value;
return;
}
if (k == null) { // 情况2:entry 不为空,但是 key 为空?弱引用,是可能出现的
replaceStaleEntry(key, value, i); // 替换元素
return;
}
i = nextIndex(i, len); // 情况3:hash 冲突:找下一个位置(开放寻址法)
e = tab[i]; // 修改 e,继续循环
}
// 3. 找到的下标位置 i 为空,直接插入元素
tab[i] = new Entry(key, value);
int sz = ++size;
// 尝试清理一些槽位,如果不需要扩容且size达到阈值,则执行rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
get() 方法实现
看了 set 的实现,再看 get 就会比较简单了,源码如下:
public T get() {
Thread t = Thread.currentThread(); // 当前线程
ThreadLocalMap map = getMap(t); // 拿到线程的 threadLocals 变量
if (map != null) { // 如果不为空,取出 Entry,如果 Entry 也不为空,返回value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 如果 threadLocals 是空,说明还未初始化,设置默认值并返回
}
5. ThreadLocal 内存泄漏问题
原因分析
ThreadLocalMap 的 key 是弱引用,但 value 是强引用,所以发生 GC 时 key 一定被回收,但是 value 不会被回收(因为线程还存在)
当线程终止时,value 才会在 GC 时被回收,问题就出现在这里,线程可能一直不会终止(线程池中的线程)
解决方案
每次使用完 ThreadLocal 后调用 remove() 方法清理
6. 使用示例
多个 ThreadLocal
通过这个示例也会明白为什么 set 源码中要传 this 了,因为线程内部可能会有多个 ThreadLocal 实例
-
定义 ThreadLocal 容器
public class RequestContextHolder { // 第一个ThreadLocal:用户ID private static final ThreadLocal<Long> CURRENT_USER_ID = new ThreadLocal<>(); // 第二个ThreadLocal:请求ID private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>(); public static void setUserId(Long userId) { CURRENT_USER_ID.set(userId); } public static Long getUserId() { return CURRENT_USER_ID.get(); } public static void setRequestId(String requestId) { REQUEST_ID.set(requestId); } public static String getRequestId() { return REQUEST_ID.get(); } public static void clear() { CURRENT_USER_ID.remove(); REQUEST_ID.remove(); } }
-
AOP 切面(网关,过滤器)
@Aspect @Component public class RequestContextAspect { // 拦截所有Controller方法 @Pointcut("execution(* com.example..controller.*.*(..))") public void controllerMethods() {} // 前置通知,设置值到 ThreadLocal 中 @Before("controllerMethods()") public void beforeController(JoinPoint joinPoint) { // 模拟从请求头获取用户ID,设置到 CURRENT_USER_ID 中 String userIdHeader = request.getHeader("X-User-Id"); RequestContextHolder.setUserId(Long.parseLong(userIdHeader)); // 生成请求ID,设置到 REQUEST_ID 中 RequestContextHolder.setRequestId(UUID.randomUUID().toString()); } // 后置通知,清除 ThreadLocal @After("controllerMethods()") public void afterController() { RequestContextHolder.clear(); } }
-
Controller、Service 中使用
AOP 切的是 controller 中的方法,会使这个请求链路调用结束才清除 ThreadLocal
@RestController public class UserController { @GetMapping("/me") public void getCurrentUser() { // 直接从ThreadLocal获取上下文 Long userId = RequestContextHolder.getUserId(); String requestId = RequestContextHolder.getRequestId(); UserProfile profile = userService.getProfile(userId); } } @Service public class UserService { public UserProfile getProfile(Long userId) { // Service 方法也算在请求链路中,所以也能获取到 String requestId = RequestContextHolder.getRequestId(); } }
SimpleDateFormat 线程安全
SimpleDateFormat 不是线程安全的,可以使用 ThreadLocal 为每个线程创建独立实例。
public class DateFormatUtil {
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return dateFormat.get().format(date);
}
}
7. 总结
- 使用 private static final 修饰 ThreadLocal 变量
- 使用后必须调用 remove() 方法清理
- 考虑使用 ThreadLocal.withInitial() 方法初始化
- 避免在大量短生命周期的线程中使用 ThreadLocal