文章目录
环境
- Java 25
- Ubuntu 24.04.1
- IntelliJ IDEA 2025.2.2 (Ultimate Edition)
ThreadLocal
顾名思义,ThreadLocal对象是线程本地的对象,这就意味着,在多线程应用中,每个线程都拥有自己独立的对象副本,以确保线程之间的数据隔离。
下面通过几个示例,来了解ThreadLocal的基本用法,注意事项,等等。
例1:基本用法
ThreadLocal<String> threadLocal = new ThreadLocal<>();
var t1 = new Thread(() -> {
threadLocal.set("aaa");
System.out.println("Thread1: " + threadLocal.get());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread1: " + threadLocal.get());
});
var t2 = new Thread(() -> {
threadLocal.set("bbb");
System.out.println("Thread2: " + threadLocal.get());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread2: " + threadLocal.get());
});
var t3 = new Thread(() -> {
threadLocal.set("ccc");
System.out.println("Thread3: " + threadLocal.get());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread3: " + threadLocal.get());
});
t1.start();
t2.start();
t3.start();
运行结果如下:
Thread1: aaa
Thread2: bbb
Thread3: ccc
Thread2: bbb
Thread3: ccc
Thread1: aaa
打印的顺序可能会变化,但无论是什么样的打印顺序, Thread1 总是和 aaa 绑定的,另外2个线程也同理。可见,3个线程拥有各自的 threadLocal 副本,修改自己的 threadLocal 副本,不会影响到其它线程。
注:本例中,主线程在启动子线程之后,无需显式等待(join)子线程结束,这是因为子线程不是daemon线程,主线程一定会等待其结束。
例2:注意事项
实际项目中,我们经常使用线程池来管理线程。由于线程池中的线程是可复用的,因此,要格外小心,一个task可能会“串用”到其它task的ThreadLocal对象。
ThreadLocal<String> threadLocal = new ThreadLocal<>();
try (ExecutorService executorService = Executors.newFixedThreadPool(1)) {
var future1 = executorService.submit(() -> {
threadLocal.set("aaa");
System.out.println("Task1: " + threadLocal.get());
});
var future2 = executorService.submit(() -> {
System.out.println("Task2: " + threadLocal.get());
});
}
本例中,在task future1 中把 threadlocal 设置为 "aaa" ,任务结束后,线程回到线程池。随后的task future2 复用了这个线程(本例中线程池里只有一个线程),由于 future1 没有清理 threadlocal 对象,导致 future2 可以直接取到 threadlocal 。
一种解决办法是, future2 在使用ThreadLocal对象前,先重新设置值。不过,更好的方式是“谁创建,谁负责”,也就说 future1 在结束时,主动释放ThreadLocal对象。
由 future1 负责释放ThreadLocal对象的另一个理由是,由于线程复用,如果 future1 结束时不释放ThreadLocal对象,该对象会一直被线程所持有,直到下一个task将其重新设置值,这显然会造成毫无必要的内存占用,而且可能是成本很高的资源,比如数据库连接、文件句柄等。
综上所述,应该尽早释放ThreadLocal对象:
var future1 = executorService.submit(() -> {
threadLocal.set("aaa");
try {
System.out.println("Task1: " + threadLocal.get());
} finally {
threadLocal.remove();
}
});
例3:remove() VS. set(null)
接上例,看下面的代码:
var future1 = executorService.submit(() -> {
threadLocal.set("aaa");
try {
System.out.println("Task1: " + threadLocal.get());
} finally {
threadLocal.set(null);
}
});
本例中,把 threadLocal.remove() 替换成 threadLocal.set(null) ,这样可以吗,有没有什么问题?
从运行结果来看,二者并没有太大区别,但从内存角度看,二者还是有一些差异的。
通过源码可知,线程维护了一个 ThreadLocalMap 对象,其entry是对ThreadLocal对象的弱引用(WeakReference),和对其值的强引用:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ......
private Entry[] table;
// ......
因此:
set(null):在table里添加了一个entry,其key是ThreadLocal对象本身,其value是nullremove():从table里把对应的entry移除
所以,ThreadLocalMap的table的entry的key是对ThreadLocal对象的弱引用。
假定存在一个entry,key是对 tl 对象的弱引用,而 tl 的value是null。
现在,table包含了1个entry,其key是tl弱引用,其value是null。
假定没有别的地方引用了 tl 。则在gc的时候, tl 对象就会被回收。
回收之后,table包含了1个enry,其key是null,value也是null。
可见,当后续不再用 tl 对象及其value时,如果采用 set(null) 来清理,则gc之后(把value对象回收了,并且把ThreadLocal对象回收了,因为是弱引用),table里仍然会留下一个key为null的entry。
如果有多个ThreadLocal对象发生这种情况,table里就会留下多个key为null的entry。这就造成了没必要的内存占用。
后续JVM会自动清理这些entry,但是总之还是有隐患。
所以,一定要用 remove() 来彻底移除entry。
事实上,在IntelliJ IDEA里,如果使用了 set(null) ,IDE会给出警报信息: 'ThreadLocal.set()' with null as an argument may cause memory leak :

例4:虚拟线程
关于Java的虚拟线程简介,参见我另一篇文档 https://blog.csdn.net/duke_ding2/article/details/152010351 。
我们知道,虚拟线程是 Thread 类的子类,它运行在操作系统的线程上,后者也称为“载体线程”。但是,虚拟线程和载体线程之间没有绑定。那么,问题来了:ThreadLocal对象,是绑定到虚拟线程的,还是绑定到载体线程的?看下面的代码:
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
for (int i = 0; i < 10; i++) {
int j = i;
var vt = Thread.ofVirtual().start(() -> {
threadLocal.set(j);
System.out.println(threadLocal.get() + " " + Thread.currentThread());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(threadLocal.get() + " " + Thread.currentThread());
});
Thread.sleep(100);
}
Thread.sleep(5000);
注:最下面的sleep操作,可以替换成对每个虚拟线程的join操作,我懒得join每个线程了,所以直接在主线程里sleep了5秒,以确保所有虚拟线程都可以完成。
运行结果如下:
0 VirtualThread[#27]/runnable@ForkJoinPool-1-worker-1
1 VirtualThread[#31]/runnable@ForkJoinPool-1-worker-1
2 VirtualThread[#32]/runnable@ForkJoinPool-1-worker-1
3 VirtualThread[#33]/runnable@ForkJoinPool-1-worker-2
4 VirtualThread[#34]/runnable@ForkJoinPool-1-worker-2
5 VirtualThread[#35]/runnable@ForkJoinPool-1-worker-2
6 VirtualThread[#36]/runnable@ForkJoinPool-1-worker-1
7 VirtualThread[#37]/runnable@ForkJoinPool-1-worker-2
8 VirtualThread[#38]/runnable@ForkJoinPool-1-worker-1
9 VirtualThread[#39]/runnable@ForkJoinPool-1-worker-2
0 VirtualThread[#27]/runnable@ForkJoinPool-1-worker-1
1 VirtualThread[#31]/runnable@ForkJoinPool-1-worker-1
2 VirtualThread[#32]/runnable@ForkJoinPool-1-worker-2
3 VirtualThread[#33]/runnable@ForkJoinPool-1-worker-1
4 VirtualThread[#34]/runnable@ForkJoinPool-1-worker-2
5 VirtualThread[#35]/runnable@ForkJoinPool-1-worker-2
6 VirtualThread[#36]/runnable@ForkJoinPool-1-worker-1
7 VirtualThread[#37]/runnable@ForkJoinPool-1-worker-2
8 VirtualThread[#38]/runnable@ForkJoinPool-1-worker-1
9 VirtualThread[#39]/runnable@ForkJoinPool-1-worker-2
可见,一共有10个并发虚拟线程,而实际的载体线程数量只有2个。然而,每个虚拟线程维护了一个自己的ThreadLocal变量副本。
总结:在虚拟线程下,ThreadLocal变量是和虚拟线程绑定的,而不是和载体线程绑定的。
虚拟线程和ThreadLocal都是JVM的产物,从侧面印证了,ThreadLocal和操作系统线程之间并无直接关系。
例5:ThreadLocal值不可继承
private static final ThreadLocal<String> tl = new ThreadLocal<>();
static void main() {
new Thread(() -> {
tl.set("abc");
System.out.println(tl.get());
new Thread(() -> {
System.out.println(tl.get());
}).start();
}).start();
}
可见,ThreadLocal对所有子线程可见,但其值不可继承。
例6:InheritableThreadLocal值可继承
private static final InheritableThreadLocal<String> tl = new InheritableThreadLocal<>();
static void main() {
new Thread(() -> {
tl.set("abc");
System.out.println(tl.get());
new Thread(() -> {
System.out.println(tl.get());
new Thread(() -> {
System.out.println(tl.get());
}).start();
}).start();
}).start();
}
运行结果如下:
abc
abc
abc
可见,InheritableThreadLocal的值是可继承的。
例7:InheritableThreadLocal值继承的一次性复制
private static final InheritableThreadLocal<String> tl = new InheritableThreadLocal<>();
static void main() {
new Thread(() -> {
tl.set("abc");
System.out.println(Thread.currentThread() + tl.get());
new Thread(() -> {
System.out.println(Thread.currentThread() + tl.get());
tl.set("def");
System.out.println(Thread.currentThread() + tl.get());
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread() + tl.get());
}).start();
}
运行结果如下:
Thread[#26,Thread-0,5,main]abc
Thread[#29,Thread-1,5,main]abc
Thread[#29,Thread-1,5,main]def
Thread[#26,Thread-0,5,main]abc
可见,子线程从父线程继承InheritableThreadLocal值,是复制了一个副本,而不是共享的。
参考
https://docs.oracle.com/en/java/javase/25/core/thread-local-variables.html
1358

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



