【Java并发】【ThreadLocal】适合初学体质的ThreadLocal

👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD

🔥 2025本人正在沉淀中... 博客更新速度++

👍 欢迎点赞、收藏、关注,跟上我的更新节奏

📚欢迎订阅专栏,专栏名《在2B工作中寻求并发是否搞错了什么》

什么是ThreadLocal?

ThreadLocal 是 Java 中用于实现线程本地存储(Thread-Local Storage)的核心类,它允许每个线程拥有自己独立的变量副本,从而在多线程环境中实现线程隔离,避免共享变量带来的线程安全问题。

  1. 线程隔离
    每个线程通过 ThreadLocal 访问变量时,实际上操作的是线程自身内存中的独立副本,其他线程无法访问。
  2. 内部存储
    ThreadLocal 的值存储在 线程对象(Thread)内部的 ****ThreadLocalMap 中,其本质是一个键值对结构:
    • KeyThreadLocal 实例(弱引用)。
    • Value:线程本地变量的值。

如何使用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的源码实现,和内存泄漏的原理。

相关推荐
慕芊妤8 分钟前
Logo语言的数据可视化
开发语言·后端·golang
南玖yy24 分钟前
数据结构C语言练习(设计循环队列)
java·c语言·数据结构
烁34726 分钟前
每日一题(小白)字符串娱乐篇16
java·开发语言·算法·娱乐·暴力
努力努力再努力wz43 分钟前
【c++深入系列】:类和对象详解(下)
java·运维·c语言·开发语言·c++
程序员小续1 小时前
git rebase 和git merge使用及区别
前端·git·后端
sunbin1 小时前
Eclipse 数据空间组件-实现简单的事件消费者-6
后端
云闲不收1 小时前
golang 计时器内存泄露问题 与 pprof 性能分析工具
开发语言·后端·golang
优雅的38度1 小时前
SpringBoot 3.0+ 整合 Swagger 3.0
java·后端
Pitayafruit2 小时前
🔥 Spring Boot 3 整合 zxing:轻松生成二维码的指南
java·spring boot·后端
雷渊2 小时前
redis如何实现发布/订阅功能?
java·后端·面试