ThreadLocal原理与内存泄漏
约 1894 字大约 6 分钟
javathreadlocal
2025-03-06
概述
ThreadLocal 提供线程本地变量,每个线程都拥有该变量的独立副本,线程之间互不影响。它通过 空间换时间 的策略避免了同步开销,常用于存储线程上下文信息,如数据库连接、用户会话、事务信息等。
基本使用
public class ThreadLocalDemo {
// 创建ThreadLocal变量
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// Java 8+ 推荐写法
private static final ThreadLocal<String> userId = new ThreadLocal<>();
public String formatDate(Date date) {
// 每个线程获取自己的SimpleDateFormat实例
return dateFormat.get().format(date);
}
public void setUserId(String id) {
userId.set(id);
}
public String getUserId() {
return userId.get();
}
public void clear() {
userId.remove(); // 务必手动清理
}
}内部数据结构
关键关系链:
Thread → Thread.threadLocals (ThreadLocalMap)
→ Entry[] table
→ Entry extends WeakReference<ThreadLocal<?>>
→ key: ThreadLocal(弱引用)
→ value: Object(强引用)核心源码
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 以this(ThreadLocal实例)为key
if (e != null) {
return (T) e.value;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 每个Thread持有自己的ThreadLocalMap
}
}ThreadLocalMap — 开放地址法
ThreadLocalMap 使用 线性探测法(开放地址法)而非链表法解决哈希冲突:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 强引用
Entry(ThreadLocal<?> k, Object v) {
super(k); // key是弱引用
value = v;
}
}
private Entry[] table;
private int size;
private int threshold; // 2/3 capacity
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len - 1);
// 线性探测
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value; // 更新
return;
}
if (k == null) {
replaceStaleEntry(key, value, i); // 替换过期条目
return;
}
}
tab[i] = new Entry(key, value);
if (++size >= threshold)
rehash();
}
}为什么不用HashMap? ThreadLocalMap 是为 ThreadLocal 量身定制的:
- 键的数量通常很少(几个到几十个)
- 线性探测法在少量元素时缓存局部性更好
- 方便在操作过程中顺便清理过期Entry
threadLocalHashCode — 魔数0x61c88647
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
// 斐波那契散列的增量(黄金分割比相关)
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}0x61c88647 对应黄金比例的整数近似值,使得生成的哈希值在2的幂大小的数组中能均匀分布。
内存泄漏问题
泄漏根因
- ThreadLocal 对象的强引用被置为 null
- 弱引用的 key 被 GC 回收 → Entry 的 key 变为 null
- 但 Entry 的 value 是强引用,仍然被 ThreadLocalMap 持有
- 如果线程不终止(线程池场景),这个 value 永远无法回收
为什么key用弱引用?
如果 key 也是强引用,那么即使 ThreadLocal 对象在外部已经没有任何引用了,ThreadLocalMap 中的 Entry 仍然会阻止 ThreadLocal 对象被回收 — 连 key 本身都泄漏了。使用弱引用至少让 key(ThreadLocal对象)能被回收,JVM在后续 get/set/remove 操作时有机会清理这些 stale Entry。
清理机制
ThreadLocalMap 在 get、set、remove 操作中都会探测性地清理 key=null 的过期 Entry:
// expungeStaleEntry — 清理过期条目
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理当前过期条目
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 继续向后探测,清理更多过期条目
Entry e;
int i;
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// rehash未过期的条目
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null) h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}但这种"惰性清理"并不可靠 — 如果后续没有 get/set 操作触发清理,过期 Entry 就会一直存在。
最佳实践:避免内存泄漏
public class SafeThreadLocalUsage {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public void process() {
try {
connectionHolder.set(getConnection());
// 业务逻辑...
} finally {
connectionHolder.remove(); // 必须在finally中remove
}
}
}
// 更好的封装方式
public class RequestContext implements AutoCloseable {
private static final ThreadLocal<RequestContext> CONTEXT = new ThreadLocal<>();
private String userId;
private String traceId;
public static RequestContext init(String userId, String traceId) {
RequestContext ctx = new RequestContext();
ctx.userId = userId;
ctx.traceId = traceId;
CONTEXT.set(ctx);
return ctx;
}
public static RequestContext current() {
return CONTEXT.get();
}
@Override
public void close() {
CONTEXT.remove(); // AutoCloseable配合try-with-resources
}
}
// 使用
try (RequestContext ctx = RequestContext.init("user1", "trace-001")) {
// 任意层级获取上下文
RequestContext.current().getUserId();
}InheritableThreadLocal
InheritableThreadLocal 允许子线程继承父线程的 ThreadLocal 值。
private static final InheritableThreadLocal<String> parentValue =
new InheritableThreadLocal<>();
parentValue.set("from parent");
new Thread(() -> {
System.out.println(parentValue.get()); // 输出 "from parent"
}).start();局限性:只在 new Thread() 创建时复制一次。如果使用线程池,线程被复用,子线程的值是第一次创建时的快照,后续提交的任务拿到的是过期值。
TransmittableThreadLocal(TTL)
阿里开源的 TransmittableThreadLocal 解决了线程池场景下的上下文传递问题。
// 依赖:com.alibaba:transmittable-thread-local
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
context.set("request-123");
ExecutorService pool = Executors.newFixedThreadPool(2);
// 包装线程池
ExecutorService ttlPool = TtlExecutors.getTtlExecutorService(pool);
ttlPool.submit(() -> {
System.out.println(context.get()); // 输出 "request-123"
});常见使用场景
| 场景 | 说明 |
|---|---|
| 数据库连接管理 | 每个线程绑定自己的Connection |
| 用户上下文 | 存储当前请求的用户信息 |
| 日志追踪 | MDC(Mapped Diagnostic Context)底层基于ThreadLocal |
| 事务管理 | Spring @Transactional底层使用ThreadLocal存储事务状态 |
| 日期格式化 | SimpleDateFormat非线程安全,ThreadLocal化 |
| 数值计算 | Random → ThreadLocalRandom 减少竞争 |
常见FAQ
Q: ThreadLocal和synchronized的区别?
A: synchronized 是多线程竞争同一个资源时的同步手段(悲观锁),ThreadLocal 是让每个线程拥有独立副本从而避免竞争。前者是"排队使用一份",后者是"每人一份"。
Q: 为什么Spring的RequestContextHolder用ThreadLocal?
A: Servlet容器为每个HTTP请求分配一个线程,ThreadLocal天然适合存储请求级别的上下文(如HttpServletRequest),在Controller/Service/DAO各层都能访问,无需层层传参。
Q: ThreadLocal的value可以是可变对象吗?
A: 可以,但需要注意:如果多个线程通过共享引用修改同一个可变对象,ThreadLocal本身并不能防止这种情况。确保 set 的是每个线程独立创建的对象。
总结
ThreadLocal 是 Java 并发编程中的重要工具,它通过线程隔离策略避免了同步开销。但必须时刻注意内存泄漏风险,尤其在线程池环境下,务必在 finally 块中调用 remove()。对于跨线程池的上下文传递,考虑使用 TransmittableThreadLocal。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于