前言
由于项目框架原因,在谨慎考虑后使用了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的方式。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。