ThreadLocal线程本地存储详解
在并发编程中,处理共享数据时通常需要同步机制(如synchronized
或Lock
)来保证线程安全。但 ThreadLocal 提供了另一种思路:让共享数据不再共享,通过为每个线程创建独立的变量副本,实现线程间的数据隔离。
ThreadLocal 核心原理
基本概念
ThreadLocal(线程本地变量)为每个线程维护一个独立的变量副本,线程对副本的修改不会影响其他线程。其核心思想是:
- 空间换时间:通过为每个线程分配独立内存空间,避免线程间的同步开销。
- 数据隔离:每个线程操作自己的副本,消除竞争条件。
实现结构
ThreadLocal 的实现依赖于Thread
类和ThreadLocalMap
:
Thread 类:每个线程持有两个ThreadLocalMap对象:
1
2ThreadLocal.ThreadLocalMap threadLocals = null; // 线程本地变量
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 可继承的本地变量ThreadLocalMap:ThreadLocal 的静态内部类,本质是一个哈希表,key 为 ThreadLocal 对象,value 为线程的变量副本。
ThreadLocal 核心方法
ThreadLocal 通过以下方法操作线程的变量副本:
get()
:获取当前线程的变量副本
1 | public T get() { |
set(T value)
:设置当前线程的变量副本
1 | public void set(T value) { |
initialValue()
:初始化变量副本(默认 null)
需重写此方法设置初始值,例如:
1 | private static ThreadLocal<Integer> myThreadLocal = new ThreadLocal<Integer>() { |
remove()
:移除当前线程的变量副本
1 | public void remove() { |
ThreadLocalMap 详解
ThreadLocalMap
是 ThreadLocal 的核心存储结构,采用开地址法解决哈希冲突,内部由Entry
数组实现。
Entry 结构
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
- key 为弱引用:当 ThreadLocal 对象无强引用时,会被 GC 回收,避免内存泄漏。
- value 为强引用:需手动调用
remove()
释放,否则可能内存泄漏。
核心方法
set(ThreadLocal<?> key, Object value)
:存储键值对
- 计算 key 的哈希值确定数组下标。
- 若发生哈希冲突,采用线性探测法(依次检查下一个位置)寻找空闲槽位。
- 若遇到 key 为 null 的 Entry(已被 GC 回收),则替换该位置的无效数据。
getEntry(ThreadLocal<?> key)
:查询值
- 先通过哈希值定位下标,若命中直接返回。
- 若未命中,通过线性探测继续查找,同时清理 key 为 null 的无效 Entry。
remove(ThreadLocal<?> key)
:移除值
- 找到 key 对应的 Entry 后,清除其 value 并标记为 null,触发 GC 回收。
线程继承性:InheritableThreadLocal
ThreadLocal
的变量无法被子线程继承,而InheritableThreadLocal
(ThreadLocal 的子类)解决了这一问题,允许子线程访问父线程的本地变量。
实现原理
重写
getMap()
和createMap()
,使用线程的inheritableThreadLocals
而非threadLocals
。子线程创建时,会复制父线程的inheritableThreadLocals到自身:
1
2
3
4// Thread类的init方法
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
应用场景
- Web 开发中,用于在子线程中获取父线程的请求上下文(如
RequestContextHolder
)。
内存泄漏问题
泄漏原因
- Entry 的 key 是弱引用,ThreadLocal 对象被回收后,key 变为 null。
- 但 value 是强引用,若线程未销毁(如线程池复用),value 会一直占用内存,导致泄漏。
解决方案
- 手动调用
remove()
:使用完 ThreadLocal 后,显式调用remove()
清除 value。 - JDK 的防护机制:
get()
、set()
、remove()
方法会自动清理 key 为 null 的 Entry。
示例代码与运行结果
示例代码
1 | public class ThreadLocalVariableHolder { |
运行结果
1 | pool-1-thread-1:1 |
每个线程的变量独立递增,互不干扰,验证了 ThreadLocal 的隔离性。
v1.3.10