必须掌握的【InheritableThreadLocal】

💬前言

我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!

最近遇到一个问题,在异步响应的请求中遇到 获取不到shiro上下文资源的情况,这个问题就引出为什么有些异步代码中又能获取到shiro的上下文资源呢?

这就引出了shiro的上下文资源是如何传递的?没错就是通过InheritableThreadLocal包装来传递的。

今天就来说一下这个属性,其中也是有很多坑儿需要注意的🤪

📃认识 InheritableThreadLocal

首先ThreadLocal大家都比较熟悉,能够把变量绑定到线程上,各线程独立维护互不影响(引用类型copy引用地址)。InheritableThreadLocal 主要是解决父子线程之间的数据传递问题。

➡使用示例

java 复制代码
public class InheritableThreadLocalDemo {
    // 创建可继承的线程本地变量,用于存储用户上下文
    private static final ThreadLocal<UserContext> userContext = new InheritableThreadLocal<>();

    // 模拟用户上下文类
    static class UserContext {
        private String userId;
        private String userName;

        public UserContext(String userId, String userName) {
            this.userId = userId;
            this.userName = userName;
        }

        @Override
        public String toString() {
            return "UserContext{userId='" + userId + "', userName='" + userName + "'}";
        }
    }
    
    
    public static void main(String[] args) {
        // 父线程设置上下文
        userContext.set(new UserContext("u123", "张三"));
        System.out.println("父线程上下文: " + userContext.get());

        // 创建子线程1 - 直接使用继承的上下文
        Thread childThread1 = new Thread(() -> {
            System.out.println("子线程1获取到的上下文: " + userContext.get());
        }, "子线程1");
        childThread1.start();

        // 清理资源
        userContext.remove();
    }
}

运行输出如下:

js 复制代码
父线程上下文: UserContext{userId='u123', userName='张三'}
子线程1获取到的上下文: UserContext{userId='u123', userName='张三'}

➡实现原理

继承机制的核心,当创建新线程时,JVM会调用Thread构造方法时候,会复制父线程的inheritableThreadLocals,伪代码如下:

java 复制代码
public class Thread implements Runnable {
........
/*
 * 存储父线程的localMap
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
........
// Thread类的构造方法,inheritThreadLocals 默认传的true
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
    // ...
    
    // 复制父线程的inheritableThreadLocals
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
            .........
    }
}

复制逻辑如下

  1. 当父线程创建子线程时
  2. JVM检查父线程的inheritableThreadLocals是否为空
  3. 如果不为空,则创建子线程的inheritableThreadLocals映射
  4. 遍历父线程的所有InheritableThreadLocal变量
  5. 调用childValue()方法获取值并存入子线程的map

💫避坑 InheritableThreadLocal

➡避免在线程池中使用InheritableThreadLocal包装的信息

线程池中的线程被重用时,残留的上下文会污染后续任务。 因为InheritableThreadLocal是在创建的时候被copy到子线程中的,所以线程池中线程的InheritableThreadLocal默认会复用第一次创建的。
代码示例如下

java 复制代码
static Executor executor = Executors.newSingleThreadExecutor();

public static void main(String[] args) {
 
    //1. 发起请求1:假设现在是 用户 张三 发起请求 
    userContext.set(new UserContext("u123", "张三"));
    executor.execute(()->{
        System.out.println("子线程获取到的上下文: " + userContext.get());
    });
    // 模拟请求1执行完成
    Thread.sleep(1000);
    // 2. 发起请求2:假设现在是 用户 李四 发起请求
    userContext.set(new UserContext("123", "李四"));
    System.out.println("父线程上下文: " + userContext.get());
    executor.execute(()->{
        System.out.println("子线程获取到的上下文: " + userContext.get());
    });
    // 清理资源
    userContext.remove();

}

输出如下:

js 复制代码
子线程获取到的上下文: UserContext{userId='u123', userName='张三'}
父线程上下文: UserContext{userId='123', userName='李四'}
子线程获取到的上下文: UserContext{userId='u123', userName='张三'}

📖所以如果我们在业务代码在线程池中获取过InheritableThreadLocal变量,比如shiro的用户信息,那么就会出现幽灵代码。明明是A用户,获取到信息可能是B用户。

✔解决方案

尽量不要异步方法中获取 InheritableThreadLocal 资源,如果非要在线程池中获取,那么就在当前线程中获取上InheritableThreadLocal 资源,传递到线程中然后替换。或者使用包装后的线程池,原理也是在异步执行前后重置InheritableThreadLocal 资源

Spring 的 ThreadPoolTaskExecutor 提供了 TaskDecorator 接口,可在任务执行前后拦截并处理上下文,是线程池环境下最优雅的解决方案。

✅推荐大家直接使用:TransmittableThreadLocal(简称 TTL)是阿里巴巴开源的一个 Java 工具类。下面的示例只是不单独引入依赖,以及方便了解 处理的原理。

🔈示例代码:

java 复制代码
@Configuration
public class TraceIdThreadPoolConfig {

    // 模拟存储TraceID的InheritableThreadLocal
    public static final InheritableThreadLocal<String> TRACE_ID = new InheritableThreadLocal<>();

    @Bean(name = "traceIdExecutor")
    public Executor traceIdExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1); // 核心线程数
        executor.setMaxPoolSize(1); // 最大线程数
        executor.setQueueCapacity(10); // 队列容量
        executor.setThreadNamePrefix("trace-"); // 线程名前缀

        // 设置TaskDecorator处理上下文
        executor.setTaskDecorator(runnable -> {
            // 1. 捕获提交任务的线程(父线程)的上下文
            String parentTraceId = TRACE_ID.get();

            // 2. 包装任务,执行前后处理上下文
            return () -> {
                String originalTraceId = null;
                try {
                    // 执行前:将父线程的上下文设置到工作线程
                    originalTraceId = TRACE_ID.get(); // 保存工作线程原有值(可能为null)
                    TRACE_ID.set(parentTraceId);

                    // 执行实际任务
                    runnable.run();
                } finally {
                    // 执行后:恢复工作线程原有值(清理上下文,避免污染)
                    if (originalTraceId == null) {
                        TRACE_ID.remove(); // 若原有值为null,直接移除
                    } else {
                        TRACE_ID.set(originalTraceId); // 否则恢复原有值
                    }
                }
            };
        });

        // 拒绝策略:当线程和队列都满时,由提交任务的线程执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

➡引用类型共享

如果InheritableThreadLocal存储可变对象,父子线程会共享同一实例

🔈示例:

java 复制代码
// 创建InheritableThreadLocal存储可变对象
private static final InheritableThreadLocal<List<String>> threadLocal =
    new InheritableThreadLocal<List<String>>() {
        @Override
        protected List<String> initialValue() {
            return new ArrayList<>();
        }
    };

public static void main(String[] args) {
    // 父线程添加数据
    threadLocal.get().add("父线程添加的数据");
    // 创建子线程
    Thread childThread = new Thread(() -> {
        // 子线程获取并修改数据
        List<String> list = threadLocal.get();
        list.add("子线程添加的数据");
    });
}

✔解决方案

重写childValue方法就行了:

java 复制代码
// 创建InheritableThreadLocal存储可变对象
private static final InheritableThreadLocal<List<String>> threadLocal =
    new InheritableThreadLocal<List<String>>() {
        @Override
        protected List<String> initialValue() {
            return new ArrayList<>();
        }
        @Override
        protected List<String> childValue(List<String> parentValue) {
            // 创建新的列表并复制元素,实现深拷贝
            return new ArrayList<>(parentValue);
        }
    };

📖总结

InheritableThreadLocal 在开发中经常遇到,只是被框架封装了,导致我们开发都忽略了这东西。在异步编程中可能发现,线程池也能通过工具类获取用户信息,测试也没测试问题,但是到了生产环境就可能出现幽灵事件。

InheritableThreadLocal解决了父子线程间数据传递的难题,同时呢也存在一些坑儿,在使用过程中下面几点我们就要掌握呢:

  1. 深入理解继承机制 :明白值是如何从父线程传递到子线程的(Thread 构造方法)
  2. 严格生命周期管理:确保及时清理避免内存泄漏
  3. 防御性编程 :对可变对象进行深拷贝(重写childValue
  4. 线程池特殊处理 :使用装饰器或专门库处理线程复用(TransmittableThreadLocal(简称 TTL))

当您掌握了这些技巧,InheritableThreadLocal将成为处理异步任务上下文传递的强大工具,特别是在安全认证、链路追踪等场景中发挥关键作用。

相关推荐
MrSYJ14 分钟前
UserDetailService是在什么环节生效的,为什么自定义之后就能被识别
java·spring boot·后端
张志鹏PHP全栈15 分钟前
Rust第一天,安装Visual Studio 2022并下载汉化包
后端
estarlee21 分钟前
公交线路规划免费API接口详解
后端
无责任此方_修行中34 分钟前
从 HTTP 轮询到 MQTT:我们在 AWS IoT Core 上的架构演进与实战复盘
后端·架构·aws
考虑考虑40 分钟前
postgressql更新时间
数据库·后端·postgresql
long3161 小时前
构建者设计模式 Builder
java·后端·学习·设计模式
吐个泡泡v2 小时前
Maven 核心命令详解:compile、exec:java、package 与 IDE Reload 机制深度解析
java·ide·maven·mvn compile
天上掉下来个程小白2 小时前
微服务-01.导入黑马商城
java·微服务·架构
Noii.2 小时前
Spring Boot初级概念及自动配置原理
java·spring boot·后端
探索java2 小时前
Tomcat Server 组件原理
java·后端·tomcat