👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD
🔥 2025本人正在沉淀中... 博客更新速度++
👍 欢迎点赞、收藏、关注,跟上我的更新节奏
📚欢迎订阅专栏,专栏名《在2B工作中寻求并发是否搞错了什么》
什么是ThreadLocal?

ThreadLocal 是 Java 中用于实现线程本地存储(Thread-Local Storage)的核心类,它允许每个线程拥有自己独立的变量副本,从而在多线程环境中实现线程隔离,避免共享变量带来的线程安全问题。
- 线程隔离
每个线程通过ThreadLocal
访问变量时,实际上操作的是线程自身内存中的独立副本,其他线程无法访问。 - 内部存储
ThreadLocal
的值存储在 线程对象(Thread)内部的 ****ThreadLocalMap
中,其本质是一个键值对结构:
-
- Key :
ThreadLocal
实例(弱引用)。 - Value:线程本地变量的值。
- Key :
如何使用TheadLocal?
非常的简单,只要记得get、set、remove就行了。
通常声明为静态变量,以便所有实例共享同一个变量,但每个线程有独立副本。
swift
private static ThreadLocal<Integer> threadId = new ThreadLocal<>();
set就是给当前线程赋值,get就是获取当前线程set的值,这个没话说
csharp
threadId.set(100); // 设置当前线程的值为100
int id = threadId.get(); // 获取当前线程的值
用完后,一定要记得用remove,不然会有内存泄漏的风险,至于为什么呢?原理篇会说。
csharp
try {
threadId.set(100);
// 执行业务逻辑
} finally {
threadId.remove();
}
最佳实践
上下文传递
在Web应用中传递用户会话(Session)或请求ID,避免显式传参。
csharp
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(); }
}
重用非线程安全对象
缓存 SimpleDateFormat
等对象,避免频繁创建。
arduino
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
资源隔离
在数据库事务中绑定连接,确保同一线程内使用同一连接。
csharp
public class ConnectionHolder {
private static final ThreadLocal<Connection> connection = new ThreadLocal<>();
public static Connection getConnection() {
if (connection.get() == null) {
connection.set(createConnection());
}
return connection.get();
}
public static void close() {
Connection conn = connection.get();
if (conn != null) conn.close();
connection.remove();
}
}
父子线程传参数问题
当我们主线程用threadlocal,向子线程传参的时候,代码会像这样写:
csharp
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("main thread info");
System.out.println("main thread get: " + threadLocal.get());
new Thread(() -> System.out.println("sub thread get: " + threadLocal.get())).start();
threadLocal.remove();
}
输出结果,我们会发现子线程获取不到数据。
arduino
main thread get: main thread info
sub thread get: null
使用InheritableThreadLocal
csharp
public static void main(String[] args) throws InterruptedException {
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("main thread info");
System.out.println("main thread get: " + threadLocal.get());
new Thread(() -> System.out.println("sub thread get: " + threadLocal.get())).start();
threadLocal.remove();
}
输出结果,完美获取。
arduino
main thread get: main thread info
sub thread get: main thread info
但是通常情况下,我们是使用线程池来创建子线程,这样就有问题了。
InheritableThreadLocal
仅在子线程创建时拷贝父线程的值,后续父线程的修改对已创建的子线程无影响。下面是代码例子:
csharp
public static void main(String[] args) throws Exception {
InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("main value 1");
System.out.println("main thread set value: " + threadLocal.get());
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.execute(() -> System.out.println("Task1, thread " + Thread.currentThread().getName()+ " get value : " + threadLocal.get()));
Thread.sleep(1000);
threadLocal.set("main value 2");
System.out.println("main thread set value: " + threadLocal.get());
executorService.execute(() -> System.out.println("Task2, thread " + Thread.currentThread().getName()+ " get value : " + threadLocal.get()));
threadLocal.remove();
executorService.shutdown();
}
输出结果,我们可以看到线程池复用了之前的线程,所以导致,
- 线程池中的线程在第一次执行任务时,会从父线程(主线程)继承
InheritableThreadLocal
的值(即mian value 1
)。 - 当父线程修改值为
main value 2
并提交第二个任务时,线程池中的线程已经是复用的,不会重新执行初始化逻辑,因此子线程仍然使用第一次继承的旧值。
arduino
main thread set value: main value 1
Task1, thread pool-1-thread-1 get value : main value 1
main thread set value: main value 2
Task2, thread pool-1-thread-1 get value : main value 1
TransmittableThreadLocal
TTL 的上下文传递需要显式地通过 TtlRunnable.get()
或 TtlCallable.get()
包装任务。如果不包装,任务执行时无法捕获父线程的上下文。
csharp
public static void main(String[] args) throws Exception {
TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();
threadLocal.set("main value 1");
System.out.println("main thread set value: " + threadLocal.get());
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 需要显示的任务
executorService.execute(TtlRunnable.get( () -> System.out.println("Task1, thread " + Thread.currentThread().getName() + " get value : " + threadLocal.get())));
Thread.sleep(1000);
threadLocal.set("main value 2");
System.out.println("main thread set value: " + threadLocal.get());
executorService.execute(TtlRunnable.get( () -> System.out.println("Task2, thread " + Thread.currentThread().getName() + " get value : " + threadLocal.get())));
threadLocal.remove();
executorService.shutdown();
}
输出结果
arduino
main thread set value: main value 1
Task1, thread pool-1-thread-1 get value : main value 1
main thread set value: main value 2
Task2, thread pool-1-thread-1 get value : main value 2
三种对比
特性 | ThreadLocal | InheritableThreadLocal | TransmittableThreadLocal (TTL) |
---|---|---|---|
数据隔离 | 线程私有 | 父子线程继承 | 父子线程继承 + 线程池支持 |
线程池兼容性 | 不支持 | 不支持 | 支持 |
数据传递方式 | 无 | 子线程创建时拷贝父线程值 | 动态传递(每次任务执行时绑定) |
适用场景 | 单线程数据隔离 | 简单父子线程传递 | 线程池、异步任务等复杂场景 |
后话
这篇仅仅是简单的使用,主播对ThreadLocal的探索并没有结束。
uu们,关注点上,下篇,主播将与你们深入源码,一起研究ThreadLocal的源码实现,和内存泄漏的原理。