侧边栏壁纸
博主头像
东家博主等级

东家不是家,心里有个她!

  • 累计撰写 8 篇文章
  • 累计创建 13 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

记一次ThreadLocal失效之异步

东家
2023-04-27 / 0 评论 / 0 点赞 / 294 阅读 / 1,760 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-07-10,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

业务场景:

业务中有一个调整奖金金额的功能,由于和钱相关,所以业务方希望能对这个功能的所有与金额相关操作都记录下来

实现思路

实现比较简单,直接校验数据后就更新数据库

public void bonusPriceAdjustment(BonusPriceAdjustmentParam bonusPriceAdjustmentParam) {
	// 校验数据
    // ……
    // 操作数据库
    // ……
}

为了不耦合业务函数,我们采用了切片的方式进行业务日志记录,具体实现如下:

1、在方法执行前,获取数据库原有数据(奖金),并存放到ThreadLocal

2、当方法执行完成后,获取当前方法的入参需要修改的奖金(奖金),再ThreadLocal中获取原有奖金,将这两个数据合并成一条变更记录数据

3、将变更数据存入数据表中

@Aspect
@Component
public class BonusAdjustmentLogAspect {
    /**
     * 线程局部变量,用于存储业务日志对象
     */
    private static final ThreadLocal<BonusPriceAdjustmentLog> BUSINESS_LOG_THREAD_LOCAL = new ThreadLocal<>();
    
    @Pointcut("execution(* cn.bonusPriceAdjustment(..))")
    public void bonusPriceAdjustment() {

    }
    @Before("bonusPriceAdjustment()")
    public void before(JoinPoint joinPoint) {
        // 查询原数据
        BonusPriceAdjustmentLog originDate =
                staffMonthlyBonusPerformanceMapper.selectLogInfoByPrimaryKey(parameter.getId());
        BUSINESS_LOG_THREAD_LOCAL.set(originDate);
    }

    @AfterReturning(value = "bonusPriceAdjustment()", returning = "result")
    @Async
    public void afterReturning(JoinPoint joinPoint, Object result) {
        BonusPriceAdjustmentParam newDate = getParameter(joinPoint);
        BonusPriceAdjustmentLog originDate = BUSINESS_LOG_THREAD_LOCAL.get();
        // 合并数据
        BusinessLogBonusAdjustment businessLogBonusAdjustment = BusinessLogBonusAdjustment.bonusPriceAdjustmentLogConvertAsThis(originDate);
        // 入库
        businessLogBonusAdjustmentMapper.insert(businessLogBonusAdjustment);
        
        BUSINESS_LOG_THREAD_LOCAL.remove();
    }

    @AfterThrowing(value = "bonusPriceAdjustment()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Throwable e) {
        BUSINESS_LOG_THREAD_LOCAL.remove();
    }
}

问题

为了减少操作数据库,阻塞请求时间,我们选择了异步处理,在afterReturning()方法上面添加了 @Async注解

这里如果使用线程池,也会有同样的问题。

当接口被调用的时候,添加了**@Async注解的方法中调用BUSINESS_LOG_THREAD_LOCAL.get()**会返回null,获取不到请求前存放的原始数据,从而导致记录变动记录处理失败

原因

这是由于@Async注解的方法会在另一个线程中执行,而ThreadLocal是线程局部变量,不同的线程之间互相独立,所以在异步方法中无法获取到之前存储在ThreadLocal中的数据。

解决思路

方法1:去掉@Async

直接去掉@Async注解,改为同一个线程处理

@AfterReturning(value = "bonusPriceAdjustment()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {

}

方法2:使用InheritableThreadLocal

为了解决这个问题,我们还可以使用InheritableThreadLocal代替ThreadLocal来存储业务日志对象。InheritableThreadLocalThreadLocal的一个扩展,它能够让子线程继承父线程的ThreadLocal变量,这样就能在异步方法中获取到之前存储在父线程中的数据。

具体做法是将代码中的ThreadLocal<BonusPriceAdjustmentLog>替换为InheritableThreadLocal<BonusPriceAdjustmentLog>,然后在业务逻辑开始前使用set方法存储数据,在异步方法中使用get方法获取数据即可。例如:

private static final InheritableThreadLocal<BonusPriceAdjustmentLog> BUSINESS_LOG_THREAD_LOCAL = new InheritableThreadLocal<>();

@Before("bonusPriceAdjustment()")
public void before(JoinPoint joinPoint) {
    // 查询原数据
    BonusPriceAdjustmentLog originDate =
        staffMonthlyBonusPerformanceMapper.selectLogInfoByPrimaryKey(parameter.getId());
    BUSINESS_LOG_THREAD_LOCAL.set(originDate);
}

@AfterReturning(value = "bonusPriceAdjustment()", returning = "result")
@Async
public void afterReturning(JoinPoint joinPoint, Object result) {
    BonusPriceAdjustmentParam newDate = getParameter(joinPoint);
    if (newDate == null) {
        LoggerUtils.errorElk(this.getClass(), "日志记录异常:接口入参为空");
    }
    BonusPriceAdjustmentLog originDate = BUSINESS_LOG_THREAD_LOCAL.get();
    // 合并数据
    BusinessLogBonusAdjustment businessLogBonusAdjustment = BusinessLogBonusAdjustment.bonusPriceAdjustmentLogConvertAsThis(originDate);
    // 记录日志
    businessLogBonusAdjustmentMapper.insert(businessLogBonusAdjustment);
    BUSINESS_LOG_THREAD_LOCAL.remove();
}

InheritableThreadLocal源码

JDK 17 LTS版本

ThreadLocalInheritableThreadLocal 都提供了一种在多线程环境下存储线程本地变量的方式,它们之间的主要区别在于线程的继承。

在使用 ThreadLocal 时,每个线程都会拥有自己的变量副本,这个变量只能由这个线程访问,其他线程无法访问。而使用 InheritableThreadLocal 时,子线程可以从父线程继承变量的值,也就是说,当一个线程创建子线程时,子线程将会拥有父线程的所有 InheritableThreadLocal 变量的副本。

ThreadLocal部分源码:

public class ThreadLocal<T> {

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal部分源码:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

Thread部分源码:

public class Thread implements Runnable {
    // ThreadLocal 实例的底层存储
    ThreadLocal.ThreadLocalMap threadLocals = null;

    // InheritableThreadLocals 实例的底层存储
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

此外我们在getMapcreateMap这两个方法中也发现InheritableThreadLocal的底层其实是用 inheritableThreadLocals来存储的,而ThreadLocal用的是 threadLocals变量存储。

知道了这些,我们再来看下创建线程时涉及到的 inheritableThreadLocals 复制相关的关键代码如下:

public class Thread implements Runnable {  
    @SuppressWarnings("removal")
    private Thread(ThreadGroup g, Runnable target, String name,
                   long stackSize, AccessControlContext acc,
                   boolean inheritThreadLocals) {
        // 省略其他代码……
        Thread parent = currentThread();
        // 省略安全校验……
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            // 将当前线程的 inheritableThreadLocals 复制给新创建线程的 inheritableThreadLocals
            this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }
}

从源码分析 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals会在创建Thread的时候将父类的inheritableThreadLocals复制到当前的Thread中,从而实现跨线程访问。

思考

  1. 线程安全ThreadLocalInheritableThreadLocal都是线程安全的,每个线程都拥有自己的变量副本,线程之间互相不干扰,不需要使用同步机制。但是,需要注意在使用这些变量时,要避免并发修改,否则可能会出现数据不一致的问题。
  2. 内存泄漏:由于ThreadLocalInheritableThreadLocal的变量只与线程有关,因此在多线程环境下,如果不及时清理变量,可能会导致内存泄漏。为了避免这种情况发生,应该在不需要使用变量时,及时将变量设置为 null
  3. 线程继承:在使用InheritableThreadLocal时,子线程会继承父线程的变量的值,这可能会导致子线程使用的变量值不是自己设置的值,而是父线程设置的值,这可能会引起一些意想不到的问题。因此,在使用 InheritableThreadLocal时,需要注意变量的作用域和生命周期。
  4. 性能:由于ThreadLocalInheritableThreadLocal都需要在每个线程中存储变量副本,因此,在多线程环境下,如果频繁地创建和销毁线程,会对性能造成一定的影响。为了避免这种情况发生,应该尽量重用线程,或者使用线程池来管理线程。

总之,在使用ThreadLocalInheritableThreadLocal时,需要注意线程安全、内存泄漏、线程继承和性能等问题。正确地使用这些变量可以提高代码的可维护性和可读性,避免一些潜在的问题。

0

评论区