我麻了。。。ThreadLocal竟然获取不到值!!!

今天项目上测试环境,再给领导演示的时候出现了bug,很尴尬。于是我跟前端同学通过模拟请求,最后发现在调一个接口的时候返回了一个 token为空 的错误。

但是前端同学说传了token了,那为什么还会报token为空的错误呢

1:问题起因

我们项目使用的JWT生成用户token,每次请求都要经过拦截器校验。因为在请求的时候我们经常需要用到当前用户登录的ID,所以我们使用到了ThhreadLocal这个工具类。

Map<String, Claim> claimMap = JwtUtil.verifyToken(headerToken);
if (null == claimMap) {
    throw new GlobalException(ResponseEnums.TOKEN_INVALID_ERROR);
}
//将参数放入上下文中
Map<String, Object> result = new HashMap<>();
Set<Map.Entry<String, Claim>> entrySet = claimMap.entrySet();
for (Map.Entry<String, Claim> claimEntry : entrySet) {
    result.put(claimEntry.getKey(), claimEntry.getValue().asString());
}
//将用户ID存到ThreadLocal中,以便后续的获取使用
ThreadLocalUtil.getInstance().setContext(result);

这里也贴上ThreadLocalUtil工具类代码

@Slf4j
public class ThreadLocalUtil {

    private static final ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<>();

    private ThreadLocalUtil() {}

    public static ThreadLocalUtil getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final ThreadLocalUtil INSTANCE = new ThreadLocalUtil();
    }

    public void setContext(Map<String, Object> map) {
        CONTEXT.set(map);
    }

    public static Map<String, Object> getContext() {
        return CONTEXT.get();
    }

    public void clear() {
        CONTEXT.remove();
    }

    public static Integer getUserId() {
        Map<String, Object> context = getContext();
        if(context == null || !context.containsKey(JwtUtil.USER_ID)) {
            throw new GlobalException(ResponseEnums.TOKEN_IS_NULL_ERROR);
        }
        return Integer.parseInt(String.valueOf(context.get(JwtUtil.USER_ID)));
    }

}

2:问题复现

我们写一个简单的测试

public static void main(String[] args) {
    HashMap<String,Object> map = new HashMap<>();
    map.put(JwtUtil.USER_ID,"1");
    ThreadLocalUtil.getInstance().setContext(map);
    System.out.println(ThreadLocalUtil.getUserId());
}

 

可以看到可以拿到我们设置的值。

但是如果将ThreadLocal跟Java8的Stream一起配合使用呢?

public static void main(String[] args) {
    HashMap<String,Object> map = new HashMap<>();
    map.put(JwtUtil.USER_ID,"1");
    ThreadLocalUtil.getInstance().setContext(map);
    
    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    //ThreadLocal获取值放在stream里面执行
    list.parallelStream().filter(x -> x.equals(ThreadLocalUtil.getUserId())).collect(Collectors.toList());
}

 

我们的问题复现了,报了token为空的错误。但是这还是随机会出现的情况,并不是每次都会出现。所以导致我们在调试的时候并没有出现这个问题

3:分析问题

咋一看并不能知道为什么会这样,所以我在获取用户id的打印了一下日志

 看出问题了吧,竟然有三个线程来获取,因为我们设置值的线程就是main线程,所以前面二个线程获取到的值就是空的,所以就抛出了异常

 

所以现在只需要知道这个 ForkJoinPool是在哪就好了,最终在翻看源码,找到原来就是在jdk8的Stream里面。

这是为什么呢?因为Jdk8的Stream底层使用了ForkJoinPool线程池,这就导致当我们调用 ThreadLocalUtil.getUserId()的时候,是直接提交到了ForkJoinPool线程池中去了,这时候就会有其它线程去调用这个方法,所以就拿不到值了

4:如何解决

解决办法就很简单了,只需要把ThreadLocalUtil.getUserId()单独拿出来执行就可以了

public static void main(String[] args) {
    HashMap<String,Object> map = new HashMap<>();
    map.put(JwtUtil.USER_ID,"1");
    ThreadLocalUtil.getInstance().setContext(map);

    List<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    //单独拿出来执行
    Integer userId = ThreadLocalUtil.getUserId();
    list.parallelStream().filter(x -> x.equals(userId)).collect(Collectors.toList());
}

所以当你项目使用到了ThreadLocal的时候,切记要单独使用,否则指不定就出现跟我一样的问题了

### 原因分析 在使用 `CompletableFuture` 和自定义线程池时,`ThreadLocal.get()` 无法获取的原因主要与线程上下文的隔离机制有关。`ThreadLocal` 的设计初衷是为每个线程提供独立的数据副本,因此一个线程设置的只能在其自身后续操作中被访问到[^1]。 当使用 `CompletableFuture.runAsync` 或 `supplyAsync` 方法时,默认情况下任务会被提交到公共的 `ForkJoinPool` 中执行。由于这些任务运行在不同于主线程的新线程上,它们无法直接继承主线程的 `ThreadLocal` 。即使显式地指定了自定义线程池,这种行为也不会改变:线程池中的线程并不自动继承提交任务线程的 `ThreadLocal` 状态。 此外,虽然 Java 提供了 `InheritableThreadLocal` 来支持父线程向子线程传递数据的功能,但在线程池场景下效果不佳。这是因为线程池中的线程通常是预先创建好的,并且会被多个任务复用。这意味着通过 `InheritableThreadLocal` 设置的可能会被后续无关的任务读取到,从而引发数据污染或安全问题[^2]。 ### 解决方案 #### 手动传递上下文 最直接的方法是在异步任务中手动将需要的上下文信息作为参数传递进去。例如,在调用 `runAsync` 或 `supplyAsync` 时,可以在 lambda 表达式内部捕获并使用当前线程的 `ThreadLocal` ,或者将该封装进任务对象中。这种方式确保了无论哪个线程执行任务,都能访问到正确的上下文数据。 ```java ThreadLocal<String> threadLocal = new ThreadLocal<>(); threadLocal.set("主线程"); ExecutorService executor = Executors.newFixedThreadPool(1); CompletableFuture.runAsync(() -> { String value = threadLocal.get(); // 输出 null System.out.println(value); }, executor).join(); ``` 上述代码中,如果希望子线程能够访问到主线程设置的 `ThreadLocal` ,则需显式地将其作为参数传入任务体: ```java String localValue = threadLocal.get(); CompletableFuture.runAsync(() -> { System.out.println(localValue); // 输出 主线程 }, executor).join(); ``` #### 使用 TransmittableThreadLocal (TTL) 对于更复杂的应用场景,推荐采用阿里巴巴开源的 [TransmittableThreadLocal](https://github.com/alibaba/transmittable-thread-local) 库来解决线程池环境下 `ThreadLocal` 传递的问题。TTL 扩展了标准库中的 `ThreadLocal`,能够在任务提交至线程池时自动复制当前线程的 `ThreadLocal` 到目标线程上,保证了跨线程的一致性。 要启用 TTL 支持,首先需要引入相关依赖,然后替换原有的 `ThreadLocal` 实例为 `TransmittableThreadLocal` 类型,并利用其提供的包装器处理异步任务。具体实现如下所示: ```xml <!-- Maven 配置 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.12.0</version> </dependency> ``` ```java import com.alibaba.ttl.TransmittableThreadLocal; import com.alibaba.ttl.TtlRunnable; // 创建可传递的 ThreadLocal 实例 TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>(); ttl.set("主线程"); // 包装 Runnable 以携带上下文信息 Runnable wrappedTask = TtlRunnable.get(() -> { System.out.println(ttl.get()); // 输出 主线程 }); ExecutorService executor = Executors.newFixedThreadPool(1); executor.submit(wrappedTask); ``` 这种方法不仅解决了 `ThreadLocal` 在线程池环境下的可见性问题,同时也避免了因不当清理而导致的内存泄漏风险。 #### 自定义线程工厂 另一种策略是通过自定义线程工厂初始化线程池,使得每次创建新线程时都能够预加载必要的 `ThreadLocal` 。不过需要注意的是,这种方式仅适用于那些不涉及线程复用的情况;一旦线程被回收再分配给其他任务,先前设置的状态将会丢失或干扰新的业务逻辑。 ```java public class ContextAwareThreadFactory implements ThreadFactory { private final Map<ThreadLocal<?>, Object> context; public ContextAwareThreadFactory(Map<ThreadLocal<?>, Object> context) { this.context = context; } @Override public Thread newThread(Runnable r) { return new Thread(r) { @Override public void start() { context.forEach((key, value) -> key.set(value)); super.start(); } }; } } // 初始化线程池时应用自定义工厂 Map<ThreadLocal<?>, Object> initialContext = Collections.singletonMap((ThreadLocal<Object>) () -> {}, "初始"); ExecutorService executor = new ThreadPoolExecutor( 2, 5, 5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), new ContextAwareThreadFactory(initialContext), new ThreadPoolExecutor.AbortPolicy() ); ``` 综上所述,针对 `CompletableFuture` 和自定义线程池中 `ThreadLocal.get()` 获取不到的问题,可以通过手动传递上下文、采用 TransmittableThreadLocal 或者自定义线程工厂等方式加以解决。选择合适的技术手段取决于具体的业务需求及系统架构设计。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值