ThreadLocal详解
什么是 ThreadLocal?
ThreadLocal 是 Java 中的一个工具类,用于为每个线程提供独立的变量副本,使得每个线程可以独立操作自己的变量,避免多线程环境下的数据竞争问题。它的核心思想是线程封闭(Thread Confinement),即通过将变量限制在线程内部使用,实现线程安全。
ThreadLocal 的核心机制
存储结构
每个线程(Thread 对象)内部维护了一个 ThreadLocalMap,键(Key)是 ThreadLocal 实例,值(Value)是该线程的局部变量副本。
// 简化的 ThreadLocal 结构
public class ThreadLocal<T> {public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = t.threadLocals;Entry e = map.getEntry(this);return (T)e.value;}
}
ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
数据隔离
不同线程访问同一个 ThreadLocal 时,实际上操作的是各自线程的 ThreadLocalMap 中的不同数据,天然线程安全。
ThreadLocal 的 API
void set(T value)
设置当前线程的变量副本
public void set(T value) {//1、获取当前线程Thread t = Thread.currentThread();//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,//则直接更新要保存的变量值,否则创建threadLocalMap,并赋值ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);else// 初始化thradLocalMap 并赋值createMap(t, value);}
ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化
T get()
获取当前线程的变量副本
public T get() {//1、获取当前线程Thread t = Thread.currentThread();//2、获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t);//3、如果map数据不为空,if (map != null) {//3.1、获取threalLocalMap中存储的值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为nullreturn setInitialValue();}
void remove()
移除当前线程的变量副本
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}
initialValue()
初始化变量(需重写,默认返回null)
private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}
ThreadLocal 的典型使用场景
全局变量在线程间隔离
例如用户会话(Session)、数据库连接、事务上下文等。
线程安全的工具类
如 SimpleDateFormat 是非线程安全的,通过 ThreadLocal 为每个线程分配独立实例:
private static final ThreadLocal<SimpleDateFormat> dateFormat =ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));public String formatDate(Date date) {return dateFormat.get().format(date);
}
跨方法传递隐式参数
避免在方法间层层传递参数,例如在日志框架中保存请求ID。
ThreadLocal 的内存泄漏问题
原因
ThreadLocalMap内部维护了一个Entry[] table来存储键值对的映射关系,内存泄漏和Entry类有非常大的关系,下面是Entry的源码:
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value; // 强引用Entry(ThreadLocal<?> k, Object v) {super(k); // Key 是弱引用value = v;}
}
ThreadLocalMap中的Entry的key是弱引用指向ThreadLocal实例,而value是强引用。(弱引用:非必须存活的对象,引用关系比软引用还弱,不管内存是否够用,下次GC一定回收)。
解决方案
自动清理机制(部分场景触发)
当调用 ThreadLocal 的 get()、set() 或 remove() 方法时,ThreadLocalMap 会尝试清理 Key 已被回收的 Entry(即 Key 为 null 的 Entry)。但这种清理是 非彻底 的:
触发条件:操作过程中发现 Key 为 null 的 Entry。
清理范围:仅清理当前操作路径上的 Entry,不会全量扫描。(从当前操作的哈希槽位(Hash Slot)开始,向后线性探测遍历,直到遇到 Entry 为 null 的槽位为止。在此路径上遇到的 所有 Key 为 null 的 Entry 会被直接清理,但其他位置的无效 Entry 可能仍残留。)
示例:
threadLocal.set(123); // 触发部分清理
threadLocal.get(); // 触发部分清理
显式调用 remove():
使用完 ThreadLocal 后,调用 remove() 清理 Entry。
使用 try-finally 块:
try {threadLocal.set(value);// ... 业务逻辑
} finally {threadLocal.remove();
}
InheritableThreadLocal
默认情况下,子线程无法继承父线程的 ThreadLocal 变量。使用 InheritableThreadLocal 解决此问题:
private static final InheritableThreadLocal<String> inheritableThreadLocal =new InheritableThreadLocal<>();public static void main(String[] args) {inheritableThreadLocal.set("Parent Value");new Thread(() -> {System.out.println(inheritableThreadLocal.get()); // 输出: Parent Value}).start();
}