文章目录
- 一、ThreadLocal
- 二、ThreadLocal的线程隔离
- 三、ThreadLocalMap
- 四、ThreadLocalg内存泄漏问题
- 五、InheritableThreadLocal:父子线程数据传递
- 实际应用场景
- 面试问题
一、ThreadLocal
在多线程环境下,多个线程访问同一个共享变量时,容易出现线程安全问题。我们通常用synchronized或Lock来加锁解决,但加锁会让线程排队执行,降低并发效率。
ThreadLocal提供了另一种思路:为每个线程创建独立的变量副本,每个线程操作自己的那份副本,互不干扰。这样既避免了线程安全问题,又无需加锁,性能更高。
- set(T value):在当前线程中设置一个值。
- get():获取当前线程中存储的值。
- remove():移除当前线程中存储的值,防止内存泄漏(非常重要) 。
java
class ThreadLocalDemo {
// 声明一个ThreadLocal变量(建议用static final修饰)
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
public static void main(String[] args) {
// 线程1:存入"张三"
new Thread(() -> {
threadLocal.set("张三");
threadLocal1.set("aaa");
System.out.println("线程1获取:" + threadLocal.get()); // 张三
System.out.println("线程1获取:" + threadLocal1.get()); // aaa
threadLocal.remove(); // 用完清理
}, "线程1").start();
// 线程2:存入"李四"
new Thread(() -> {
threadLocal.set("李四");
System.out.println("线程2获取:" + threadLocal.get()); // 李四
threadLocal.remove();
}, "线程2").start();
// 主线程:没有存过,获取为null
System.out.println("主线程获取:" + threadLocal.get()); // null
}
}
输出结果:线程1打印"张三",线程1的threadLocal1打印"aaa",线程2打印"李四",主线程打印"null"------三个线程互不影响。
二、ThreadLocal的线程隔离
ThreadLocal线程隔离核心:每个线程Thread对象内部,都持有一个ThreadLocalMap
- Thread 对象持有 ThreadLocal.ThreadLocalMap 类型的成员变量
- ThreadLocalMap 内部是一个 Entry[] 数组
- Entry 继承自 WeakReference<ThreadLocal<?>>,key 是对 ThreadLocal 实例的弱引用,value 是存入的数据(强引用)

1.set方法源码
set 方法先拿到当前线程的引用 Thread.currentThread(),通过 getMap(t) 获取该线程内部的 threadLocals(即 ThreadLocalMap)。如果 Map 已存在,以当前 ThreadLocal 实例为 key,将 value 存入。如果 Map 不存在(线程第一次使用 ThreadLocal),则创建一个新的 ThreadLocalMap 并存入数据。
java
// ThreadLocal.set() 简化源码
public void set(T value) {
Thread t = Thread.currentThread();// 1. 获取当前线程
ThreadLocalMap map = getMap(t); // 2. 获取当前线程的ThreadLocalMap
if (map != null)
map.set(this, value); // 3. 以当前ThreadLocal为key存值
else
createMap(t, value); // 4. 首次创建ThreadLocalMap
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals; // 直接返回Thread对象的成员变量
}
数据实际存储在线程Thread对象的threadLocals中,ThreadLocal本身只是一个key
2.get方法源码
get方法会先获取当前线程。然后获取当前线程的 ThreadLocalMap。以当前 ThreadLocal 实例为 key,查找对应的 Entry。找到则返回 value。如果 Map 为 null 或 Entry 为 null,调用 setInitialValue() 初始化默认值(默认返回 null,可重写 initialValue() 方法)
java
// ThreadLocal.get() 简化源码
public T get() {
Thread t = Thread.currentThread(); // 1. 获取当前线程
ThreadLocalMap map = getMap(t); // 2. 获取当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 3. 以this为key查找Entry
if (e != null) {
return (T) e.value; // 4. 返回value
}
}
return setInitialValue(); // 5. Map未初始化则设置初始值
}
三、ThreadLocalMap
ThreadLocalMap 是 ThreadLocal 的静态内部类,是一个自定义的哈希表,专门用来存储线程局部变量。它的 key 是 ThreadLocal 实例,value 是线程存储的数据。
ThreadLocalMap 的 Entry 继承自 WeakReference<ThreadLocal<?>>
java
static class Entry extends WeakReference<ThreadLocal<?>> {
/** 存入的值(强引用) */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // key是弱引用
value = v; // value是强引用
}
}
-
key 使用弱引用,如果 key 是强引用,即使我们在代码中把 ThreadLocal 对象设为 null,ThreadLocalMap 中的 Entry 仍然强引用了它,导致它无法被GC回收,从而造成内存泄漏。
-
使用弱引用后,当 ThreadLocal 对象除了弱引用外再无其他强引用时,GC 会回收它,Entry 的 key 变为 null
1.ThreadLocalMap的哈希冲突
在 ThreadLocal 中采取的是开放地址法的方法来解决哈希冲突。当遇到哈希冲突时,会再次进行线性探测,探测的意思其实就是去寻找下一个空位。
发生哈希冲突时:
- 先计算目标位置 index = key.threadLocalHashCode & (len - 1)
- 如果该位置已被占用
├── key 相同 → 直接覆盖
└── key 不同 → 向后查找下一个位置(index+1, index+2, ...) - 直到找到空位或相同的 key
2.过期Key的探测式清理
ThreadLocal 的设计者意识到了内存泄漏的风险,在 set、get 等方法中内置了探测式清理机制。当发现 key 为 null 的过期 Entry 时,会主动将其 value 置为 null,帮助 GC 回收。
java
// expungeStaleEntry(i) 的核心逻辑(简化)
private int expungeStaleEntry(int staleSlot) {
// 1. 将当前位置的 value 置为 null,Entry 置为 null
tab[staleSlot].value = null;
tab[staleSlot] = null;
// 2. 向后遍历,检查并清理其他过期 Entry
// 3. 对未过期的 Entry 进行 rehash,填补空位
// ...
}
四、ThreadLocalg内存泄漏问题
内存泄漏是指程序中不再使用的对象无法被 GC 回收,始终占用内存,久而久之可能导致 OutOfMemoryError。
当 ThreadLocal 对象的外部强引用断开(比如生命周期结束或被手动置为 null)时,GC 会回收它。此时 Entry 的 key 变成 null,但 value 仍然被 Entry 强引用,无法被回收。此时
- 如果线程随之结束:Thread 对象被回收,ThreadLocalMap 也被回收,value 自然释放,没问题。
- 如果线程长期存活(如线程池) :ThreadLocalMap 会一直存在,value 堆积越来越多,最终导致内存泄漏甚至 OOM。
最直接的办法就是使用完ThreadLocal后,在finally块中手动调用remove()方法。remove() 方法会主动将 ThreadLocalMap 中该 Entry 的 key 和 value 都清除。
java
// 正确的使用姿势
public class ThreadLocalBestPractice {
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
public void process(User user) {
userHolder.set(user);
try {
// 执行业务逻辑,通过 userHolder.get() 获取当前线程的用户对象
} finally {
userHolder.remove(); // ⚠️ 一定要在 finally 中清理!
}
}
}
五、InheritableThreadLocal:父子线程数据传递
普通的 ThreadLocal 无法在父子线程间传递数据:主线程存的值,子线程拿不到。InheritableThreadLocal 解决了这个问题,在创建子线程的瞬间,它将父线程的本地变量复制给子线程。
- main 线程是父线程,new Thread 创建的是子线程。
java
public class InheritableThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final InheritableThreadLocal<String> inheritableTL =
new InheritableThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("父线程-threadLocal的值");
inheritableTL.set("父线程-inheritableTL的值");
new Thread(() -> {
// ThreadLocal:拿不到父线程的数据
System.out.println("子线程获取ThreadLocal:" + threadLocal.get());
// InheritableThreadLocal:可以拿到
System.out.println("子线程获取Inheritable:" + inheritableTL.get());
}).start();
}
}

实际应用场景
1.数据库连接管理
每个线程使用自己的 Connection,避免事务混乱和连接竞争
java
public class ConnectionHolder {
private static final ThreadLocal<Connection> connHolder = new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = connHolder.get();
if (conn == null) {
conn = DriverManager.getConnection(url, user, password);
connHolder.set(conn);
}
return conn;
}
public static void closeConnection() {
Connection conn = connHolder.get();
if (conn != null) {
conn.close();
connHolder.remove();
}
}
}
2.用户会话信息存储
在Web应用中,将当前请求的用户信息存入ThreadLocal,整个请求链路无需显式传递用户对象
java
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setUser(User user) {
currentUser.set(user);
}
public static User getUser() {
return currentUser.get();
}
public static void clear() {
currentUser.remove();
}
}
面试问题
1.一个线程可以有多个ThreadLocal对象吗?
- 可以。线程内部的ThreadLocalMap是一个数组,不同的ThreadLocal实例通过哈希计算定位到数组的不同位置,所以一个线程可以持有多个ThreadLocal对象
2.ThreadLocalMap的key是如何计算出来的?
- 每个 ThreadLocal 实例在创建时,都会从静态的 AtomicInteger 计数器中获取一个唯一的 threadLocalHashCode。这个值不是简单的自增,而是每次加上一个黄金分割数(0x61c88647),目的是让哈希值分布更加均匀,减少冲突。
- 计算数组下标的公式:
java
int i = key.threadLocalHashCode & (table.length - 1);
- 这里 table.length 总是 2 的幂,所以 (table.length - 1) 相当于取低几位,和 HashMap 的索引计算原理一致。
3.ThreadLocal的get()方法执行时,发生GC后key是否为null?
- 如果外部强引用被回收,那么发生GC后,Entry的key(弱引用)会被置为null,但value仍然存在。此时调用get()方法,ThreadLocalMap会通过探测式清理机制跳过key为null的Entry,然后返回setInitialValue()的默认值。
4.ThreadLocal的set()和get()方法源码中用到了什么设计模式?
- initialValue()方法是一个protected方法,默认返回null,交给子类去重写,这是典型的模板方法模式。spring的RequestContextHolder等框架大量使用了这种模式【这里不太懂,用通俗的语言解释】
5.InheritableThreadLocal是什么?和ThreadLocal有何区别?
- InheritableThreadLocal继承自ThreadLocal,可以在创建子线程时自动将父线程的本地变量传递给子线程。区别在于:ThreadLocal操作threadLocals,InheritableThreadLocal操作inheritableThreadLocals。传递只在子线程创建时发生一次,线程池环境下不适用。
