ThreadLocal如何解决父子线程间通信问题?(上下文传递)

ThreadLocal如何解决父子线程间通信问题?

在Java多线程编程中,父子线程之间的数据传递和共享问题(上下文传递)一直是一个非常重要的议题。如果不处理好数据的传递和共享,会导致多线程程序的性能下降或者出现线程安全问题。ThreadLocal是Java提供的一种解决方案,可以非常好地解决父子线程数据共享和传递的问题。


IlnheritableThreadLocal实现父子线程传递上下文

在Thread类中存在IlnheritableThreadLocal变量,简单的说就是使用InheritableThreadLocal来进行传递。当父线程的InheritableThreadLocal不为空时,就会将这个值传到当前子线程的InheritableThreadLocal。

public class main {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal<>();
        threadLocal.set("Hi ThreadLocal");

        ThreadLocal inheritableThreadLocal = new InheritableThreadLocal();
        inheritableThreadLocal.set("Hi InheritableThreadLocal");

        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLocal.get());
                System.out.println(inheritableThreadLocal.get());
            }
        });
        t.start();
    }
}

/*
输出:
null
Hi InheritableThreadLocal
*/

但是要注意,对于在主线程中使用线程池将主线程上下文传递到子线程时,会出现以下问题

  • 线程池复用:线程池中的线程是复用的,不同的任务可能在同一个线程上执行。如果前一个任务在这个线程上设置了 InheritableThreadLocal 的值,而后一个任务没有显式清除或设置该值,那么后一个任务会意外地访问到前一个任务传递的上下文。
  • 值的隔离性:由于 InheritableThreadLocal 只在创建子线程时进行初始化,因此如果你在主线程中设置了它,只有在子线程创建时(即新线程被分配到没有正在执行的任务的线程池线程时),父线程的上下文才会传递过来。这种传递的缺失可能导致上下文信息不一致,特别是在多次提交任务的情况下。
  • 性能开销:虽然 InheritableThreadLocal 的使用能在一定程度上简化上下文传递,但它的生命周期通常较长,可能导致内存泄漏,尤其在长时间运行的应用程序中,如果旧的上下文信息没有被清理,还会增加系统的负担。
  • 繁琐。业务逻辑要知道:有哪些上下文;各个上下文是如何获取的。并需要业务逻辑去一个一个地捕捉与传递。
  • 调试复杂性:当出现上下文穿透(即意外地继承了前一个任务的上下文),尤其是现在的分布式微服务系统,debug 和维护代码就变得复杂,因为你需要追踪状态在不同任务之间是如何传递的。

为了避免这些问题,可以考虑在任务中显式地传递所需的上下文信息,而不是依赖于多个线程之间的隐式传递。然后通过一些设计去做统一化处理,减少繁琐的代码逻辑,比如下面介绍的两种方法。



线程装饰器实现主线程和线程池上下文传递

在主线程上下文传递到线程池的场景中使用InheritableThreadLocal 会出现上面说的很多问题。

而线程池在设计的时候就考虑了上下文传递问题,所以在ThreadPoolTaskExecutor(spring线程池类)中有个叫做TaskDecorator的装饰器, Spring 已经为你处理好了许多潜在的问题。

这个装饰器的作用就是在任务的执行前后做一点事情,于是我们可以自定义个装饰器来实现TaskDecorator接口,目的就是在现场任务执行前把上下文信息传递给子线程,在任务执行后将上下文信息清除。下面通过案例说明:

//自定义用户上下文
public class UserContext {
    private static final ThreadLocal<UserContext> userContext = new ThreadLocal<>();
    private String userId;
    public static void setUserContext(UserContext context) {
        userContext.set(context);
    }
    public static UserContext getUserContext() {
        return userContext.get();
    }
    public static void clear() {
        userContext.remove();
    }
    public String getUserId() {
        return userId;
    }
    public void setUserId(String userId) {
        this.userId = userId;
    }
}

自定义装饰器:

import org.springframework.core.task.TaskDecorator;
public class UserContextTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        UserContext userContext = UserContext.getUserContext(); // 获取主线程的上下文信息
        return () -> {
            try {
                UserContext.setUserContext(userContext); // 在子线程中设置上下文信息
                runnable.run(); // 执行任务
            } finally {
                UserContext.clear(); // 清理上下文信息
            }
        };
    }
}

在线程池配置中将装饰器set进去

@Configuration
@EnableAsync // 启用异步支持
public class AppConfig {
    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.setTaskDecorator(new UserContextTaskDecorator()); // 设置 TaskDecorator
        executor.initialize();
        return executor;
    }
}

然后就可以通过线程池异步调用了:

@Service
public class UserService {
    public void performTask() {
        UserContext context = new UserContext();
        context.setUserId("user123");// 项目中可能是通过拦截器获取用户token
        UserContext.setUserContext(context); // 设置主线程的上下文
        asyncMethod(); // 调用异步方法
    }

    @Async("taskExecutor") // 指定使用的线程池
    public void asyncMethod() {
        UserContext currentContext = UserContext.getUserContext();
        System.out.println("Current User ID: " + currentContext.getUserId());
    }
}


阿里开源项目Transmittable ThreadLocal(TTL)实现主线程和线程池上下文传递

还是上面的例子,通过TLL框架也可以实现,TransmittableThreadLocal继承了IlnheritableThreadLocal,最终的父类还是ThreadLocal,因此只需要在使用的时候将对象的创建由ThreadLocal调整为TransmittableThreadLocal,然后在线程池的地方也不需要使用线程装饰器来传递了,只需要在最后使用TtlExecutor包装一层, 你就不需要再关注ThreadLocal线程间数据传递的事情了。下面继续用上面的案例说明:

在项目中添加TTL的依赖,可以通过Maven来管理依赖。然后接着,我们定义一个自定义的用户上下文类和使用TransmittableThreadLocal来保存上下文信息。

// 自定义用户上下文
public class UserContext {
    private static final TransmittableThreadLocal<UserContext> userContext = new TransmittableThreadLocal<>();
    private String userId;
    public static void setUserContext(UserContext context) {
        userContext.set(context);
    }
    public static UserContext getUserContext() {
        return userContext.get();
    }
    public static void clear() {
        userContext.remove();
    }
    public String getUserId() {
        return userId;
    }
    public void setUserId(String userId) {
        this.userId = userId;
    }
}

在使用TransmittableThreadLocal的上下文中,我们可以直接在线程池配置中使用TtlExecutors来包装线程池的执行器。

@Configuration
@EnableAsync // 启用异步支持
public class AppConfig {
    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        executor.initialize();

        // 使用TtlExecutors包装线程池
        return TtlExecutors.getTtlExecutor(executor);
    }
}

然后就可以通过线程池异步调用了,就是这么简单,框架的好处是已经帮你把很多繁琐的问题考虑好了。

@Service
public class UserService {
    public void performTask() {
        UserContext context = new UserContext();
        context.setUserId("user123"); // 项目中可能是通过拦截器获取用户token
        UserContext.setUserContext(context); // 设置主线程的上下文
        asyncMethod(); // 调用异步方法
    }

    @Async("taskExecutor") // 指定使用的线程池
    public void asyncMethod() {
        UserContext currentContext = UserContext.getUserContext();
        System.out.println("Current User ID: " + currentContext.getUserId());
    }
}
  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值