用这4招 优雅的实现Spring Boot 异步线程间数据传递

原创 蜗牛 程序员蜗牛g 2023-02-18 12:03 发表于江苏

点击上方“程序员蜗牛g”,选择“设为星标”

跟蜗牛哥一起,每天进步一点点

程序员蜗牛g

大厂程序员一枚 跟蜗牛一起 每天进步一点点

9篇原创内容

公众号

你好,我是蜗牛!

在实际开发中需要在父子线程之间传递一些数据,比如用户登录信息使用ThreadLocal存放保证线程隔离,代码如下:

/**
 * @author 公众号:程序员蜗牛g
 * @description 用户上下文信息
 */
public class UserUtils {
    private static  final  ThreadLocal<String> userLocal=new ThreadLocal<>();

    public static  String getUserId(){
        return userLocal.get();
    }
    public static void setUserId(String userId){
        userLocal.set(userId);
    }

    public static void clear(){
        userLocal.remove();
    }}

那么子线程想要获取这个userId如何做呢?

1. 手动设置

代码如下:

public void handlerAsync() {
    //1. 获取父线程的userId
        String userId = UserUtils.getUserId();
        CompletableFuture.runAsync(()->{
            //2. 设置子线程的值,复用
            UserUtils.setUserId(userId);
            log.info("子线程的值:{}",UserUtils.getUserId());
        });    }

这样子每次开异步线程都需要手动设置,重复代码太多,看了头疼!

2. 线程池设置TaskDecorator

TaskDecorator是一个执行回调方法的装饰器,主要应用于传递上下文,或者提供任务的监控/统计信息。

那我们该如何去使用?代码如下:

 
/**
* @author 公众号:程序员蜗牛g
* @description 上下文装饰器
*/
public class CustomTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
String robotId = UserUtils.getUserId();
System.out.println(robotId);
return () -> {
try {
// 将主线程的请求信息,设置到子线程中
UserUtils.setUserId(robotId);
// 执行子线程,这一步不要忘了
runnable.run();
} finally {
// 线程结束,清空这些信息,否则可能造成内存泄漏
UserUtils.clear();
}
}; }
}

TaskDecorator需要结合线程池使用,实际开发中异步线程建议使用线程池,只需要在对应的线程池配置一下,代码如下:

 
 
@Bean(name = "asyncServiceExecutor")
public Executor asyncServiceExecutor() {
    log.info("start asyncServiceExecutor----------------");
    //ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    //使用可视化运行状态的线程池
    ThreadPoolTaskExecutor executor = new VisiableThreadPoolTaskExecutor();
    //配置核心线程数
    executor.setCorePoolSize(corePoolSize);
    //配置最大线程数
    executor.setMaxPoolSize(maxPoolSize);
    //配置队列大小
    executor.setQueueCapacity(queueCapacity);
    //配置线程池中的线程的名称前缀
    executor.setThreadNamePrefix(namePrefix);

    // rejection-policy:当pool已经达到max size的时候,如何处理新任务
    // CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

    //增加线程池修饰类
    executor.setTaskDecorator(new CustomTaskDecorator());
    //增加MDC的线程池修饰类
    //executor.setTaskDecorator(new MDCTaskDecorator());
    //执行初始化
    executor.initialize();
    log.info("end asyncServiceExecutor------------");
    return executor;
}

此时业务代码就不需要去设置子线程的值,直接使用即可,代码如下:

 
public void handlerAsync() {
log.info("父线程的用户信息:{}", UserUtils.getUserId());
//执行异步任务,需要指定的线程池
CompletableFuture.runAsync(()->
     log.info("子线程的用户信息:{}", UserUtils.getUserId()),
    taskExecutor);
}

来看一下结果,如下图:

图片

这里使用的是CompletableFuture执行异步任务,使用@Async这个注解同样是可行的。

注意:无论使用何种方式,都需要指定线程池

3. InheritableThreadLocal

InheritableThreadLocal虽然能够实现父子线程间的复用,但是在线程池中使用会存在复用的问题

这种方案使用也是非常简单,直接用InheritableThreadLocal替换ThreadLocal即可,代码如下:

 
/**
* @author 公众号:程序员蜗牛g
* @description 用户上下文信息
*/
 
public class UserUtils {
    private static  final  InheritableThreadLocal<String>  threadLocal = new InheritableThreadLocal<>();

    public static  String get(){
        return threadLocal.get();
    }
    public static void set(String userId){
        threadLocal.set(userId);
    }
    public static void clear(){
        threadLocal.remove();
    }
}
 
 

4. TransmittableThreadLocal

TransmittableThreadLocal是由阿里开发的一个线程变量传递工具包,解决了InheritableThreadLocal只能再new Thread的时候传递本地变量,无法应用到线程池的问题。可以应用来作链路追踪,传递变量等用途,下面我们来了解一下原理。

使用起来也是非常简单,添加依赖如下:

<dependency>
 <groupId>com.alibaba</groupId>
 <artifactId>transmittable-thread-local</artifactId>
 <version>2.14.2</version>
</dependency>

UserUtils改造代码如下:

 
 
/**
 * @author 公众号:程序员蜗牛g
 * @description 用户上下文信息
 */
public class UserUtils  {
    private static  final TransmittableThreadLocal<String> threadLocal =new TransmittableThreadLocal<>();

    public static  String get(){
        return threadLocal.get();
    }
    public static void set(String userId){
        threadLocal.set(loginVal);
    }
    public static void clear(){
        threadLocal.remove();
    }
}

TransmittableThreadLocal原理

 TransmittableThreadLocal继承自InheritableThreadLocal,因此它可以在创建线程的时候将值传递给子线程,那么怎么确保使用线程池的时候也有效呢?我们来看一下源码

1、构造方法

 public TransmittableThreadLocal() {
    this(false);
 }

 public TransmittableThreadLocal(boolean disableIgnoreNullValueSemantics) {
    // 是否忽略null值set,默认false
    this.disableIgnoreNullValueSemantics = disableIgnoreNullValueSemantics;
 }

2、set方法

   

 
/**
 * {@inheritDoc}
 */
@Override
public final void set(T value) {
    if (!disableIgnoreNullValueSemantics && value == null) {
        // may set null to remove value
        remove();
    } else {
        super.set(value);
        addThisToHolder();
    }
}
 

  

先看addThisToHolder方法

@SuppressWarnings("unchecked")
private void addThisToHolder() {
    if (!holder.get().containsKey(this)) {
        holder.get().put((TransmittableThreadLocal<Object>) this, null);
     // WeakHashMap supports null value.
    }
}

属性holder又是什么呢?

 
 
 
 

1、final static修饰的变量,只会存在一份

2、使用了WeakHashMap,弱引用,方便垃圾回收

3、key就是TransmittableThreadLocal对象

    remove方法

/**
 * {@inheritDoc}
 */
@Override
public final void remove() {
    removeThisFromHolder();
    super.remove();
}

4、get方法

/**
 * {@inheritDoc}
 */
@Override
public final T get() {
    T value = super.get();
    if (disableIgnoreNullValueSemantics || value != null) addThisToHolder();
    return value;
}

5、当我们使用线程池时,需要使用TtlRunnable.get(runnable)对runnable进行包装,或者使用TtlExecutors.getTtlExecutor(executor)对执行器进行包装,才能使线程池的变量传递起效果,那么我们就接着看一下源码的执行流程

TtlExecutors.getTtlExecutor(executor)

public static Executor getTtlExecutor(@Nullable Executor executor) {
    if (TtlAgent.isTtlAgentLoaded() || null == executor || executor instanceof TtlEnhanced) {
        return executor;
    }
    // 包装执行器
    return new ExecutorTtlWrapper(executor, true);
}
ExecutorTtlWrapper(@NonNull Executor executor, boolean idempotent) {
    this.executor = executor;
    this.idempotent = idempotent;
}
public void execute(@NonNull Runnable command) {
    // 实际上也是通过TtlRunnable对原runnable进行包装
    executor.execute(TtlRunnable.get(command, false, idempotent));
}

可以看到,两种方式原理一样,我们直接看TtlRunnable.get()

public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {

    if (null == runnable) return null;
    if (runnable instanceof TtlEnhanced) {
        if (idempotent) return (TtlRunnable) runnable;
        else throw new IllegalStateException("Already TtlRunnable!");
    }
    // 返回TtlRunnable
    return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
}

构建TtlRunnable

private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
      // 原子引用
      this.capturedRef = new AtomicReference<Object>(capture());
      this.runnable = runnable;
      this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
  }

capture捕获父线程的ttl

     

public static Object capture() {
    return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}

private static
HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
    HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value =
        new HashMap<TransmittableThreadLocal<Object>, Object>();
    // 遍历了所有holder
 for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
  // copyValue实际上调用了TransmittableThreadLocal的get方法获取线程存储的变量值
    ttl2Value.put(threadLocal, threadLocal.copyValue());
 }
  return ttl2Value;
}

private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {
    final HashMap<ThreadLocal<Object>, Object> threadLocal2Value =
new HashMap<ThreadLocal<Object>, Object>();
    for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry :
      threadLocalHolder.entrySet()) {
        final ThreadLocal<Object> threadLocal = entry.getKey();
        final TtlCopier<Object> copier = entry.getValue();
        threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get()));
    }
    return threadLocal2Value;
}

再看TtlRunnable的run方法

public void run() {
    // 获取Snapshot对象,里面存储了父线程的值
    final Object captured = capturedRef.get();
    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
    // 传入capture方法捕获的ttl,然后在子线程重放,也就是调用ttl的set方法,
    // 这样就会把值设置到当前的线程中去,最后会把子线程之前存在的ttl返回
    final Object backup = replay(captured);
    try {
        // 调用原runnable的run
        runnable.run();
    } finally {
        // 
        restore(backup);
    }
}

总结

上述列举了4种方案,蜗牛这里推荐方案2和方案4,其中两种方案的缺点非常明显,实际开发中也是采用的方案2或者方案4。

 

图片

参考

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值