1.ThreadLocal有什么用
ThreadLocal的主要作用是为每个线程提供独立的变量副本,实现线程隔离。这里副本其实相当于用的不是实例,而是拷贝的意思;
看一段代码再来理解一下:
public class ThreadLocal1 {
private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
private static final ThreadLocal<Integer> countThreadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 线程1
new Thread(() -> {
userThreadLocal.set("User1");
countThreadLocal.set(100);
System.out.println(userThreadLocal.get()); // 输出 User1
System.out.println(countThreadLocal.get()); // 输出 100
}).start();
// 线程2
new Thread(() -> {
userThreadLocal.set("User2");
countThreadLocal.set(200);
System.out.println(userThreadLocal.get()); // 输出 User2
System.out.println(countThreadLocal.get()); // 输出 200
}).start();
}
}
这里声明了两个ThreadLocal,然后分别在两个线程使用;为了理解变量副本,把第二个Thread的 userThreadLocal.set("User2");注释掉,输出为null;
// 线程2
new Thread(() -> {
//userThreadLocal.set("User2");
countThreadLocal.set(200);
System.out.println(userThreadLocal.get()); // 输出 null
System.out.println(countThreadLocal.get()); // 输出 200
}).start();
可能有的人会说因为线程1并没有对userThreadLocal赋值,其实并不是,最后输出如下:
User1
100
null
200
其实就是因为每个线程的ThreadLocal是独立的
2.ThreadLcoal原理
https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html#%E2%AD%90%EF%B8%8Fthreadlocal-%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97
为了解释上面的结果,接下来从Thread,ThreadLocal源码来解释;
ThreadLocal.java
public ThreadLocal() {
}
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
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);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals = null;
set方法:
可以看到是通过获取当前线程然后通过当前线程t获取ThreadLocalMap,从这里就可以知道最终存取变量的是Thread中的ThreadLocalMap变量;
接下来看一下ThreadLocalMap的源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;//ThreadLocal的真正的值
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
也就是说最终使用Entry(用来存取键值对),
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
看下面一幅图就知道了:

ThreadLocal最终是属于拥有他的线程的ThreadLocalMap;
3.优势是什么
3.1. 实现真正的线程隔离
ThreadLocal的核心目的是让每个线程拥有自己独立的变量副本。通过将数据存储在Thread对象内部的ThreadLocalMap中,实现了:
线程A访问ThreadLocal时,自动从线程A自己的ThreadLocalMap中获取数据
线程B访问同一个ThreadLocal时,从线程B自己的ThreadLocalMap获取数据
两个线程互不干扰,天然线程安全
3.2. 避免竞争开销
如果采用全局哈希表(比如用ConcurrentHashMap存储所有线程的ThreadLocal数据):
Map<Thread, Map<ThreadLocal, Object>> globalMap
每次访问都需要:
获取当前线程对象
通过线程对象查二级Map
还需要处理并发问题(锁竞争)
而现在的设计:
1.每个线程直接持有一个ThreadLocalMap
2.get/set操作只需要访问当前线程的局部变量(无锁)
3.性能接近直接访问普通变量
3.3 与线程生命周期绑定
ThreadLocalMap的生命周期与Thread绑定:
线程终止时,ThreadLocalMap会被自动回收
避免了内存泄漏管理复杂度(虽然仍需注意弱引用问题)
天然符合"线程局部变量"的语义
4. 数据结构优化
ThreadLocalMap是专门优化的定制哈希表:
使用开放寻址法而非链地址法(更适合少量数据)
Key(ThreadLocal对象)使用弱引用,避免内存泄漏
自动清理失效Entry(set/get时触发探测式清理)
4.使用
实际应用场景示例
4.1. 日期格式化(SimpleDateFormat线程安全使用)
public class DateFormatter {
// 非线程安全用法
// private static SimpleDateFormat unsafeFormat = new SimpleDateFormat("yyyy-MM-dd");
// 线程安全用法
private static final ThreadLocal<SimpleDateFormat> safeFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return safeFormat.get().format(date);
}
}
4.2. 用户上下文传递(Web应用)
public class UserContextHolder {
private static final ThreadLocal<User> context = new ThreadLocal<>();
public static void setUser(User user) {
context.set(user);
}
public static User getUser() {
return context.get();
}
public static void clear() {
context.remove();
}
}
// 在拦截器中设置用户信息
class AuthInterceptor {
public boolean preHandle(HttpServletRequest request) {
User user = getUserFromRequest(request);
UserContextHolder.setUser(user);
return true;
}
}
// 在业务层直接获取
class OrderService {
public void createOrder() {
User currentUser = UserContextHolder.getUser();
// 使用用户信息...
}
}
5.内存泄露问题
5.1什么是内存泄露
内存泄漏指的是程序申请的内存没有被正确释放,导致这部分内存无法被再次使用,最终可能引发 OOM(OutOfMemoryError)。
ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
threadLocal.set(new byte[1024 * 1024]); // 存 1MB 数据
// 假设不再使用 threadLocal,但没有 remove()
threadLocal = null; // ThreadLocal 对象被回收(key=null),但 value 还在!
5.2ThreadLocal的内存泄露
本质是由于他的内部设计导致的;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k); // 关键点:调用父类WeakReference的构造方法
value = v;
}
}
这里ThreadLocal作为k,调用了父类的构造方法;
造成内存泄露的本质两点原因:
- 用于存储的
Entry中的key值是ThreadLocal是弱引用。这意味着,如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null。
对于弱引用会被解释为:
弱引用是 Java 中的一种引用类型,它比普通的强引用(Strong Reference)更弱,不会阻止垃圾回收器(GC)回收对象。当一个对象仅被弱引用指向(没有强引用指向它)时,垃圾回收器会在下一次 GC 时自动回收该对象,即使程序仍然持有这个弱引用。
可以看看下面这段代码来理解:
public static void main(String[] args) {
Object strongRef = new Object(); // 强引用
System.gc(); // 触发 GC
System.out.println(strongRef); // 仍然能打印对象,因为强引用存在
WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.gc(); // 触发 GC
System.out.println(weakRef.get()); // 可能输出 null,因为只有弱引用
}
//下面是 WeakReference.java
public T get() {
return this.referent;
}
其中WeakReference.get()返回的就是当前引用,可以看到弱引用直接在gc回收直接变为null了,而强引用不会被回收(除非显示的置为null);
- 然后value是强引用,即使key被GC回收为null,但value 仍然被 ThreadLocalMap.Entry 强引用存在,无法被 GC 回收。
所以,当 ThreadLocal 实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap 中,因为 Entry 对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap 也会一直存在,导致 key 为 null 的 entry 无法被垃圾回收,即会造成内存泄漏。
也就是说,内存泄漏的发生需要同时满足两个条件:
- ThreadLocal 实例不再被强引用;
- 线程持续存活,导致 ThreadLocalMap 长期存在。虽然 ThreadLocalMap 在 get(), set() 和 remove() 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。
5.3如何避免内存泄漏的发生?
- 在使用完 ThreadLocal 后,务必调用 remove() 方法。 这是最安全和最推荐的做法。 remove() 方法会从 ThreadLocalMap 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 ThreadLocal 定义为 static final,也强烈建议在每次使用后调用 remove()。
ThreadLocal
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap
/**
* 移除指定 ThreadLocal 键对应的 Entry
* @param key 要移除的 ThreadLocal 键
*/
private void remove(ThreadLocal<?> key) {
// 获取哈希表数组和长度
Entry[] tab = table;
int len = tab.length;
// 计算初始哈希槽位(通过哈希码取模)
int i = key.threadLocalHashCode & (len-1); // 等价于 key.hashCode() % len
// 遍历可能发生哈希冲突的槽位(开放寻址法)
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 检查当前 Entry 的 key 是否匹配
if (e.get() == key) {
// 1. 清除弱引用(将 key 置为 null)
e.clear();
// 2. 清理 stale entry 并重新哈希后续元素
expungeStaleEntry(i);
// 3. 直接返回(不继续遍历)
return;
}
}
// 若未找到匹配的 key,直接结束(无操作)
}
- 在线程池等线程复用的场景下,使用 try-finally 块可以确保即使发生异常,remove() 方法也一定会被执行。
273

被折叠的 条评论
为什么被折叠?



