一、ThreadLocal 基本原理
1.1 ThreadLocal 是什么?
ThreadLocal 是Java中提供的一种线程隔离机制,它允许每个线程拥有自己的变量副本,不同线程之间互不影响。
java
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 每个线程操作自己的副本
new Thread(() -> {
threadLocal.set(100);
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
// 输出: Thread-0: 100
}).start();
new Thread(() -> {
threadLocal.set(200);
System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
// 输出: Thread-1: 200
}).start();
// 主线程有自己的副本
System.out.println("main: " + threadLocal.get()); // 输出: main: 0
}
}
1.2 核心数据结构
ThreadLocal 的核心在于 ThreadLocalMap,它是 Thread 类的一个内部类:
java
// Thread 类中的关键属性
public class Thread implements Runnable {
// 每个线程都有自己的ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
// ThreadLocalMap 结构
static class ThreadLocalMap {
// Entry继承自WeakReference,key是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key是弱引用
value = v; // value是强引用
}
}
private Entry[] table;
private int size;
private int threshold;
}
1.3 源码分析
1.3.1 set() 方法原理
java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 以ThreadLocal自身作为key,value作为值
map.set(this, value);
} else {
createMap(t, value);
}
}
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) {
// 遇到key为null的过期条目,执行替换
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
1.3.2 get() 方法原理
java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取当前ThreadLocal对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果不存在,初始化
return setInitialValue();
}
二、内存泄漏的根本原因
2.1 引用关系图
java
强引用: Thread -> ThreadLocalMap -> Entry[] -> Entry -> value
弱引用: Entry -----(弱引用)-----> ThreadLocal (key)
2.2 内存泄漏的产生过程
步骤1:正常使用
java
public class MemoryLeakDemo1 {
public static void main(String[] args) {
ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();
threadLocal.set(new BigObject()); // 创建大对象
// 此时引用关系:
// threadLocal(强引用) -> ThreadLocal对象
// Entry.key(弱引用) -> ThreadLocal对象
// Entry.value(强引用) -> BigObject对象
}
}
步骤2:ThreadLocal被回收(弱引用特性)
java
public class MemoryLeakDemo2 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();
threadLocal.set(new BigObject(1024 * 1024)); // 1MB对象
// 将threadLocal置为null
threadLocal = null;
// 手动触发GC
System.gc();
// 此时:
// 1. ThreadLocal对象只有Entry.key的弱引用
// 2. GC后,ThreadLocal对象被回收
// 3. Entry.key变为null
// 4. 但Entry.value仍然引用着BigObject对象!
});
thread.start();
thread.join();
// 线程结束后,问题更严重
// 线程对象可能被回收,但如果是线程池,线程复用...
}
}
2.3 为什么会有内存泄漏?
java
public class ThreadLocalMemoryLeak {
// 模拟问题场景
static class BigObject {
byte[] data;
BigObject(int size) {
this.data = new byte[size];
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.execute(() -> {
ThreadLocal<BigObject> local = new ThreadLocal<>();
local.set(new BigObject(10 * 1024 * 1024)); // 10MB
// 使用后忘记remove(),并且local超出作用域
// local = null; // 即使设置为null
// 线程执行完毕,但线程池线程不会销毁
// 下一次任务来的时候,这个Entry还在!
});
Thread.sleep(1000);
// 再次提交任务,使用同一个线程
executor.execute(() -> {
// 这个线程的ThreadLocalMap中还有一个Entry:
// key: null (已回收)
// value: 10MB的BigObject (无法访问,但占用内存)
// 更糟的是,如果这个任务又创建了新的ThreadLocal...
ThreadLocal<String> newLocal = new ThreadLocal<>();
newLocal.set("hello");
// 现在ThreadLocalMap有两个Entry
});
executor.shutdown();
}
}
三、四种引用类型与ThreadLocal
3.1 Java的四种引用类型
java
public class ReferenceTypes {
public static void main(String[] args) {
// 1. 强引用 - 不会被GC回收
Object strongRef = new Object();
// 2. 软引用 - 内存不足时回收
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 3. 弱引用 - GC时立即回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 4. 虚引用 - 用于对象回收跟踪
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
}
}
3.2 ThreadLocalMap中的Entry设计
java
// Entry继承自WeakReference,key是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // value是强引用!
Entry(ThreadLocal<?> k, Object v) {
super(k); // 调用WeakReference的构造,key是弱引用
value = v;
}
}
设计思考:
-
为什么key用弱引用? 防止ThreadLocal对象本身内存泄漏
-
为什么value用强引用? 保证在使用期间value不被回收
-
带来的问题: key被回收后,value成为"孤儿",无法被访问但占用内存
四、避免内存泄漏的最佳实践
4.1 必须使用remove()
java
public class SafeThreadLocalUsage {
private static final ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public Connection getConnection() {
Connection conn = connectionHolder.get();
if (conn == null) {
conn = createConnection();
connectionHolder.set(conn);
}
return conn;
}
public void closeConnection() {
Connection conn = connectionHolder.get();
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// 处理异常
} finally {
// 关键:必须remove!
connectionHolder.remove();
}
}
}
// 使用try-finally确保remove
public void executeWithConnection() {
try {
Connection conn = getConnection();
// 使用conn执行业务
} finally {
closeConnection();
}
}
}
4.2 使用ThreadLocal的正确模式
java
public class ThreadLocalTemplate {
// 模式1:声明为private static final
private static final ThreadLocal<SimpleDateFormat> dateFormatHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// 模式2:使用包装类管理
static class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void set(User user) {
currentUser.set(user);
}
public static User get() {
return currentUser.get();
}
public static void clear() {
currentUser.remove();
}
}
// 使用示例
public void processUser(User user) {
UserContext.set(user);
try {
// 执行业务逻辑
doBusiness();
} finally {
// 确保清理
UserContext.clear();
}
}
}
4.3 线程池中的特殊处理
java
public class ThreadPoolSafeUsage {
private static final ThreadLocal<String> requestIdHolder = new ThreadLocal<>();
private static final ExecutorService executor = Executors.newCachedThreadPool();
public void handleRequest(String requestId) {
// 提交任务前设置ThreadLocal
requestIdHolder.set(requestId);
try {
executor.execute(() -> {
try {
// 线程池线程中获取
String id = requestIdHolder.get();
process(id);
} finally {
// 线程池任务必须清理!
requestIdHolder.remove();
}
});
} finally {
// 提交线程也清理
requestIdHolder.remove();
}
}
// 更好的方案:使用包装器
public void handleRequestBetter(String requestId) {
executor.execute(new ThreadLocalTask(requestId));
}
class ThreadLocalTask implements Runnable {
private final String requestId;
ThreadLocalTask(String requestId) {
this.requestId = requestId;
}
@Override
public void run() {
// 不使用ThreadLocal,通过构造参数传递
process(requestId);
}
}
}
五、源码中的保护机制

5.1 ThreadLocalMap的清理机制
ThreadLocalMap在以下时机尝试清理过期Entry:
java
// 1. set() 时的清理
private void set(ThreadLocal<?> key, Object value) {
// ... 遍历过程中如果发现k==null,调用replaceStaleEntry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 2. get() 时的清理
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i); // 清理过期Entry
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
// 3. remove() 时的清理
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear(); // 清除弱引用
expungeStaleEntry(i); // 清理
return;
}
}
}
5.2 expungeStaleEntry 清理方法
java
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 1. 清理当前槽位
tab[staleSlot].value = null; // 释放value引用
tab[staleSlot] = null; // 清空槽位
size--;
// 2. 重新哈希后续的Entry,直到遇到null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 发现过期Entry,清理
e.value = null;
tab[i] = null;
size--;
} else {
// 重新计算哈希位置
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;
}
六、诊断和检测内存泄漏
6.1 使用工具检测
java
public class ThreadLocalLeakDetector {
public static void checkThreadLocalLeak() {
Thread thread = Thread.currentThread();
ThreadLocalMap map = thread.threadLocals;
if (map != null) {
java.lang.reflect.Field tableField;
try {
tableField = map.getClass().getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(map);
int staleCount = 0;
int totalCount = 0;
for (Object entry : table) {
if (entry != null) {
totalCount++;
// 通过反射获取Entry的referent字段(key)
java.lang.reflect.Field referentField =
entry.getClass().getSuperclass().getDeclaredField("referent");
referentField.setAccessible(true);
Object key = referentField.get(entry);
if (key == null) {
staleCount++;
System.err.println("发现过期Entry,可能存在内存泄漏!");
}
}
}
System.out.println("ThreadLocalMap统计:");
System.out.println("总Entry数: " + totalCount);
System.out.println("过期Entry数: " + staleCount);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
6.2 使用JVM参数监控
java
# 1. 启用GC详细日志
java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
# 2. 启用堆转储
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump
# 3. 使用jcmd分析
jcmd <pid> GC.class_histogram | grep ThreadLocal
jcmd <pid> Thread.print
# 4. 使用jmap分析
jmap -histo:live <pid> | grep ThreadLocal
6.3 使用Arthas诊断
java
# 安装并启动Arthas
java -jar arthas-boot.jar
# 查看线程的ThreadLocalMap
thread <thread-id>
thread -i 1000 <thread-id> # 查看线程详细信息
# 监控内存使用
dashboard # 查看实时仪表板
heapdump # 生成堆转储
# 反编译查看ThreadLocalMap
jad java.lang.ThreadLocal
jad java.lang.ThreadLocal$ThreadLocalMap
七、高级话题:InheritableThreadLocal
7.1 父子线程传递
java
public class InheritableThreadLocalDemo {
private static InheritableThreadLocal<String> inheritableThreadLocal =
new InheritableThreadLocal<>();
public static void main(String[] args) {
inheritableThreadLocal.set("父线程的值");
Thread childThread = new Thread(() -> {
System.out.println("子线程获取: " + inheritableThreadLocal.get());
// 子线程修改不影响父线程
inheritableThreadLocal.set("子线程修改");
System.out.println("子线程修改后: " + inheritableThreadLocal.get());
});
childThread.start();
try {
childThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("父线程保持原值: " + inheritableThreadLocal.get());
inheritableThreadLocal.remove(); // 同样需要清理
}
}
7.2 线程池中的问题
java
public class InheritableThreadLocalWithPool {
// 注意:线程池中复用线程,InheritableThreadLocal只在创建时复制一次
private static final InheritableThreadLocal<String> context =
new InheritableThreadLocal<>();
private static final ExecutorService executor = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
// 问题:线程池线程第一次创建后,后续任务继承的是最初的值
executor.execute(() -> {
context.set("第一次任务");
System.out.println("任务1: " + context.get());
context.remove();
});
// 第二次提交任务
executor.execute(() -> {
// 这里获取的是null,不是"第一次任务"!
System.out.println("任务2: " + context.get());
});
executor.shutdown();
}
}
八、总结
8.1 ThreadLocal内存泄漏要点
| 问题点 | 原因 | 解决方案 |
|---|---|---|
| key弱引用 | ThreadLocal被回收后key为null | 使用static final修饰ThreadLocal |
| value强引用 | key为null后value仍存在 | 必须调用remove()清理 |
| 线程池复用 | 线程不销毁,Entry一直存在 | 任务结束时清理ThreadLocal |
| InheritableThreadLocal | 父子线程传递 | 谨慎使用,注意线程池场景 |
8.2 黄金法则
-
声明为static final:防止ThreadLocal对象被回收
javaprivate static final ThreadLocal<Object> holder = new ThreadLocal<>(); -
必须调用remove():在finally块中清理
javatry { // 使用ThreadLocal } finally { holder.remove(); } -
避免在线程池中存储大对象:考虑其他方案传递参数
-
定期检查:使用监控工具检查ThreadLocal使用情况
8.3 替代方案
java
// 方案1:使用参数传递(推荐)
executor.submit(new TaskWithParam(data));
// 方案2:使用线程安全的上下文类
class RequestContext {
private static final ThreadLocal<Map<String, Object>> context =
ThreadLocal.withInitial(HashMap::new);
public static void put(String key, Object value) {
context.get().put(key, value);
}
public static void clear() {
context.get().clear();
context.remove(); // 双重清理
}
}
// 方案3:使用Scope类(如Spring的RequestScope)
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
// 每个请求独立的实例
}
记住: ThreadLocal是强大的工具,但必须正确使用。当你不确定时,优先考虑使用参数传递或创建新的对象实例,而不是依赖ThreadLocal。