标签:# 线程池

业务逻辑编排错误 & TTL浅拷贝导致参数丢失问题

前言 在DDD项目中,为了方便参数的传递,通常会使用ThreadLocal来保存一个对象来实现对参数的跨方法传递,避免通过形参的形式传递。在内部项目中,有一个项目使用的是 alibaba开源的 transmittable-thread-local来存储参数,新建了一个上下文对象(AbilityContext.java),使用HashMap来临时存储和获取参数。 AbilityContext 示例 public class AbilityContext { private static final ThreadLocal<Map<String, Object>> CONTEXT = new TransmittableThreadLocal<>(); private AbilityContext() { } /** * 初始化上下文 */ public static void initContext() { Map<String, Object> con = CONTEXT.get(); if (con == null) { CONTEXT.set(new HashMap<>(8)); } else { CONTEXT.get().clear(); } } /** * 清除上下文 */ public static void clearContext() { CONTEXT.remove(); } public static Map<String, Object> getInnerMap() { return CONTEXT.get(); } /** * 获取上下文内容 */ public static <T> T getValue(String key) { Map<String, Object> con = CONTEXT.get(); if (con == null) { return null; } return (T) con.get(key); } /** * 设置上下文参数 */ public static void putValue(String key, Object value) { Map<String, Object> con = CONTEXT.get(); if (con == null) { CONTEXT.set(new HashMap<>(8)); con = CONTEXT.get(); } con.put(key, value); } } 项目情况介绍 通常来说,DDD项目的基本流程是由interface->application,中间封装一层来集中处理上下文的初始化和清空动作,如下图: 在正常情况下,上述流程可以正确的完成参数的写入和获取,但是,在项目运行过程中遇到了一个bug,正常写入参数后,偶现性(低频)获取值为NULL,导致程序出错,示例代码如下(隐去业务代码,重新写的伪代码): 其中demo()方法为当时复现的方法 demo2()为伪代码,是业务代码中调用了另一个application,假设其逻辑和demo()方法一致的业务代码。 @Slf4j public class AlibabaTtlWrongUsageExampleApplication { public static void main(String[] args) { demo(i); } private static void demo(int idx) { // 初始化 AbilityContext.initContext(); // 赋业务值 AbilityContext.putValue("main", "mainValue"); // 这里简化了代码,实际上经过了很多层业务代码调用后才出现了此方法 ThreadUtil.execute(() -> { execute->demo2(); }); // do something // 主线程再次获取业务值(偶现为null) String value = AbilityContext.getValue("main"); if (Objects.isNull(value)) { log.warn("lastGetNullValue, idx={}", idx); } } } 上述代码运行设置了一个key=main,值为mainValue。在下方AbilityContext.getValue("main")偶现获取==NULL。 展开分析 当时在分析的开始有推测是业务代码中参数被重新赋值为NULL,但通过对后续业务代码逐行查看,并没有找到重新赋值的逻辑。 在深入业务代码分析的过程中,发现主流程中有一个异步方法调用(ThreadUtil.execute()),再次调用了另一个领域服务(这是不符合DDD规范的!),而领域服务的入口都会AbilityContext.initContext()的逻辑,通过这个线索 ,继续展开了深入分析。 编码者的初衷可能是想到异步线程已经脱离了当前线程,再次调用 initContext()方法是初始化了一个新的对象上下文,但是由于项目使用的是 alibaba TTL,能够实现跨线程的传递,所以在子线程中依旧能拿到父线程的HashMap。并且TTL默认是使用的浅拷贝对象。由于initContext()中,调用了HashMap.clear()方法,相当于将父线程的HashMap给清空了!。 通过比对父子线程的hashCode值确定为同一对象 // 主线程获取hashCode final int hashCode = AbilityContext.getInnerMap().hashCode(); ThreadUtil.execute(() -> { // 子线程对比hashCode log.info("{}, ThreadUtil hashCode={}", idx, AbilityContext.getInnerMap().hashCode() == hashCode); // 子线程再次初始化(错误的根源) AbilityContext.initContext(); // do something }); 14:42:28.198 [pool-1-thread-26] INFO top.imyzt.learning.caseanalysis.ttl.AlibabaTtlWrongUsageExampleApplication -- 25, ThreadUtil hashCode=true 持续分析 有了上述的线索,基本把问题原因找到了,但是为什么是偶现的呢? 因为使用了异步线程,而线程的调度由操作系统的线程调度算法来决定,并不是一定保证顺序的,所以只要当操作系统优先调度异步线程,那么HashMap就被清空了,如果主线程优先往下走,那么就能够获取到完整的HashMap。 后记 至此,问题分析就告一段落了,整个过程中涉及到 TTL值的父子线程传递、对象浅拷贝、线程的调度,还涉及到了DDD的不规范逻辑编排,整个分析下来花费了一上午的时间,收获还是很大的。 transmittable-thread-local TransmittableThreadLocal的传递只有浅拷贝吗? 线程的优先级 我将源代码上传了GitHub,如果你想在本地调试运行上述案例,可以下载到本地调试,有问题可以评论区沟通。
Read More ~

ThreadPoolExecutor “非常用” 方法

平时在使用线程池时,更多关注到的是coreSize、maxSize、blockQueue、RejectedExecutionHandler这些参数,但在线程池监控领域,还需要关注到其他的一些方法。在此处做统一记录和备忘: public static void main(String[] args) { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1024), new ThreadPoolExecutor.CallerRunsPolicy()); // 启动所有核心线程(预热) threadPoolExecutor.prestartAllCoreThreads(); // 启动一个核心线程 threadPoolExecutor.prestartCoreThread(); // 默认情况下构造器中的keepAliveTime指定的是非核心线程的空闲时间, 通过如下方法, 可以允许核心线程超时 threadPoolExecutor.allowCoreThreadTimeOut(true); // ⭐️ 动态线程池必备方法 // 启动后, 设置核心线程数量 threadPoolExecutor.setCorePoolSize(3); // 启动后, 设置最大线程数量 threadPoolExecutor.setMaximumPoolSize(10); // 已执行完的任务总数 threadPoolExecutor.getTaskCount(); // 获取工作队列剩余数量 threadPoolExecutor.getQueue().remainingCapacity(); } 后记 通过上面的代码可知,在运行过程中我们也是可以操作coreSize和maxSize的。那么如何才能实现对Queue的大小进行控制呢?目前开源届常用的是采取RabbitMQ中的VariableLinkedBlockingQueue来实现。
Read More ~