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

相关推荐
kangkang-2 小时前
PC端基于SpringBoot架构控制无人机(三):系统架构设计
java·架构·无人机
界面开发小八哥4 小时前
「Java EE开发指南」如何用MyEclipse创建一个WEB项目?(三)
java·ide·java-ee·myeclipse
ai小鬼头4 小时前
Ollama+OpenWeb最新版0.42+0.3.35一键安装教程,轻松搞定AI模型部署
后端·架构·github
idolyXyz4 小时前
[java: Cleaner]-一文述之
java
一碗谦谦粉4 小时前
Maven 依赖调解的两大原则
java·maven
萧曵 丶5 小时前
Rust 所有权系统:深入浅出指南
开发语言·后端·rust
netyeaxi5 小时前
Java:使用spring-boot + mybatis如何打印SQL日志?
java·spring·mybatis
收破烂的小熊猫~5 小时前
《Java修仙传:从凡胎到码帝》第四章:设计模式破万法
java·开发语言·设计模式
猴哥源码5 小时前
基于Java+SpringBoot的动物领养平台
java·spring boot
老任与码5 小时前
Spring AI Alibaba(1)——基本使用
java·人工智能·后端·springaialibaba