乐趣区

关于threadlocal:ThreadLocal与ForkJoin使用踩坑记录

前言

因为我的项目框架起因,在审慎思考后应用了 ThreadLocal 存储了上下文。上线一段时间后,发现有些时候拿到的上下文并不是本人线程的上下文,最初定位到是因为应用了 java8 的并行流,并且在并行流外面拿了一次 ThreadLocal 的上下文值,剖析了一波起因后,将并行流改为了串行流(或应用ThreadPoolExecutor),状况得以失常。

事件起因

因为我的项目构造起因,每次申请过去,咱们零碎都须要申请另外一个零碎获取用户身份信息(dubbo 外部调用,不可应用 token 之类存储)。所以咱们做了一个切面,应用注解的模式在进入此代理对象的办法时,在切面调用另一个零碎,并把用户信息封装好,放入 ThreadLocal 外面,前面办法外面能够间接在 ThreadLocal 外面获取,切面完结的时候会在 finnaly 外面移除 ThreadLocal 的值。

切面:

@Aspect
@Component
@Slf4j
public class InterfaceMonitorAspect {
    // 调用三方零碎的 Repository
    @Resource
    private BountyRepository bountyRepository;
    
    @Pointcut("@annotation(com.shizhuang.duapp.finance.loan.center.application.aop.InterfaceMonitor)")
    public void doMonitor() {log.info("doMonitor 开始...");
    }
    
    @Around("doMonitor()")
    public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable {
        ...
        try{
        ...
         // 切面外面调用三方接口
        AccountEntity accountEntity = bountyRepository.queryAccountInfo(userId, bizIdentity);
        // 切面外面设置进 Threadlocal
        AccountContextHolder.setAccountEntity(accountEntity);
        Object retVal = joinPoint.proceed();
        ...
        }catch (Throwable t) {...}finally{
        // 移除
         AccountContextHolder.clear();}
    }
}

上下文 AccountContextHolder


/**
 * create by liuliang * on 2020/12/30 4:46 PM */
 public class AccountContextHolder {
    // 可被继承的 ThreadLocal
    private static final ThreadLocal<AccountEntity> contextHolder = new InheritableThreadLocal<>();
 /**
 * 设置 accountEntity * * @param accountEntity
 */
 public static void setAccountEntity(AccountEntity accountEntity) {contextHolder.set(accountEntity);
 }
    /**
 * 获得 accountEntity * * @return
 */
 public static AccountEntity getAccountEntity() {return contextHolder.get();
 }
    /**
 * 革除上下文数据 */ public static void clear() {contextHolder.remove();
 }
}

问题所在地:

// 这里应用 java8 并行流(forkjoin 的形式)loanApplyNos.parallelStream().forEach(loanApplyNo -> {
    
      ...
        // 获取了上下文信息, 获取的 accountEntity 偶尔会不对
       AccountEntity accountEntity =  AccountContextHolder.getAccountEntity();
      ...
});

起因剖析

要晓得起因,须要理解 forkjoin 的原理,forkjoin 其核心思想就是分而治之。应用递归的思维将一个小人物拆分为多个小工作,直到达到进行拆分的条件。

并且他每个线程(线程数默认为 cpu 数)都有一个有限的执行队列。线程会从执行队列外面取工作执行。并且执行过程中,如果某些线程执行的快,为了利用 cpu,闲暇的线程会偷取其余队列外面的线程,拿到本人队列并执行。当然,为了防止竞争,队列应用的是双向队列,本人线程从队列头获取工作,偷取工作从队列尾部获取。这里我找了一张图很好的形容了一下这个场景:

剖析到这里,其实下面咱们生产上呈现的问题,就很好解释了。线程 1 的队列外面的绿色工作,设置进去的是线程 1 的上下文信息,而应用 forkjoin 后,被线程 2 执行,线程 2 获取上下文信息的时候,拿到的是线程 1 的上下文信息(此时线程 1 还有其余业务逻辑在执行,也就是 没走到切面完结的 clear 办法!这也是偶尔呈现的外围起因)。问题起因找到。

解决思路

1. 最简略的解决办法就是,如果业务容许,能够将并行流执行改为串行流执行,这样不会呈现上述情况,咱们通过思考后,走的此种计划。
2. 应用多线程,然而不应用 forkjoin 的形式。

退出移动版