一、认识ThreadLocal
ThreadLocal
是一种线程本地变量的实现方式。它的核心思想是:为每个线程都创建一个独立的变量副本,这样每个线程只能访问到自己线程中的副本,而无法访问到其他线程的副本,从而实现了线程之间的隔离。这种方式有点像“拿空间换时间”,通过为每个线程分配独立的存储空间,避免了线程之间的竞争和同步开销,相比使用 synchronized
的方式,性能更高。
二、ThreadLocal 的底层
ThreadLocal
内部有一个静态内部类 ThreadLocalMap
,它是一个基于哈希表的存储结构,用来保存线程本地变量的副本。ThreadLocalMap
中有一个 Entry
数组,每个 Entry
保存了一个键值对:
-
Key:指向
ThreadLocal
对象的弱引用(Weak Reference)。这意味着当ThreadLocal
对象没有其他强引用时,它可能会在垃圾回收时被回收。 -
Value:线程本地变量的实际值。
三、Thread Local存在的缺点
虽然 Entry
的键(Key)是弱引用,可以在垃圾回收时被回收,但仍然可能会出现内存泄漏的问题。具体来说:
-
当
ThreadLocal
对象被垃圾回收后,Entry
的键(Key)会变成null
,但此时Entry
的值(Value)可能仍然存在。 -
如果线程继续运行,这些键为
null
但值仍然存在的Entry
无法被访问,也无法被垃圾回收,从而导致内存泄漏。
四、解决办法及示例
建议在使用完 ThreadLocal
后,手动调用 remove()
方法来删除对应的 Entry
,释放内存。
public class UserSession {
// 定义一个 ThreadLocal 变量
private static final ThreadLocal<String> userId = new ThreadLocal<>();
// 设置用户 ID
public static void setUserId(String id) {
userId.set(id);
}
// 获取用户 ID
public static String getUserId() {
return userId.get();
}
// 清除用户 ID
public static void clearUserId() {
userId.remove();
}
}
// 线程任务
class UserTask implements Runnable {
private String userId;
public UserTask(String userId) {
this.userId = userId;
}
@Override
public void run() {
// 设置用户 ID
UserSession.setUserId(userId);
System.out.println("Thread " + Thread.currentThread().getName() + " is processing user: " + UserSession.getUserId());
// 模拟业务逻辑
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 清理用户 ID
UserSession.clearUserId();
System.out.println("Thread " + Thread.currentThread().getName() + " has cleared user ID.");
}
}
public class ThreadLocalExample {
public static void main(String[] args) {
// 创建线程
Thread t1 = new Thread(new UserTask("user1"), "Thread-1");
Thread t2 = new Thread(new UserTask("user2"), "Thread-2");
// 启动线程
t1.start();
t2.start();
}
}
示例解释:
-
ThreadLocal 的使用:
每个线程通过UserSession.setUserId()
设置自己的用户 ID。每个线程通过UserSession.getUserId()
获取自己的用户 ID。每个线程在任务完成后通过UserSession.clearUserId()
清理自己的用户 ID。 -
线程隔离:
线程 1 和线程 2 各自操作自己的用户 ID,互不干扰。ThreadLocal
为每个线程创建了一个独立的用户 ID 副本,存储在ThreadLocalMap
中。 -
内存泄漏的预防:
在线程任务结束时,调用了UserSession.clearUserId()
,这会删除ThreadLocalMap
中对应的Entry
,避免了内存泄漏。