AtomicBoolean + CAS 机制 关于 线程池任务丢弃策略优化
背景在审查GridItemPushSender类时发现同事使用AtomicBoolean CAS机制来控制任务的串行执行。虽然功能正确但存在更简洁的解决方案。原始实现CAS 方案代码示例publicclassGridItemPushSender{privatefinalExecutorServicesingleWorker;privatefinalAtomicBooleanisRunningnewAtomicBoolean(false);publicvoidsend(){// CAS 尝试获取执行权if(!isRunning.compareAndSet(false,true)){log.info(任务正在执行中本次请求跳过);return;}singleWorker.submit(()-{try{doSend();}catch(Exceptione){log.error(推送异常: {},e.getMessage());}finally{// 释放执行权isRunning.set(false);}});}}问题分析代码冗余需要手动管理isRunning标志位容易出错如果忘记在finally中重置标志会导致后续所有请求被拒绝职责不清线程池本身就有队列管理机制重复造轮子维护成本高增加了额外的状态变量提高了理解难度优化方案线程池内置机制核心思路利用ThreadPoolExecutor的有界队列 拒绝策略来实现相同的功能让线程池自己管理任务丢弃逻辑。优化后代码publicclassGridItemPushSender{privatefinalExecutorServicesingleWorker;publicGridItemPushSender(GridItemPushLogGatewaypushLogGateway,ProgramGatewayapiClient){this.pushLogGatewaypushLogGateway;this.apiClientapiClient;// 使用 ThreadPoolExecutor 自定义配置this.singleWorkernewThreadPoolExecutor(1,// 核心线程数1,// 最大线程数0L,TimeUnit.MILLISECONDS,// 空闲线程存活时间newArrayBlockingQueue(1),// 容量为1的有界队列Executors.defaultThreadFactory(),newThreadPoolExecutor.DiscardPolicy()// 拒绝策略直接丢弃);}publicvoidsend(){singleWorker.submit(()-{try{doSend();}catch(Exceptione){log.error(推送异常: {},e.getMessage());}});}}工作原理对比CAS 方案的工作流程请求1 → CAS检查(isRunningfalse) → 成功 → 设置isRunningtrue → 执行任务 请求2 → CAS检查(isRunningtrue) → 失败 → 直接返回 请求3 → CAS检查(isRunningtrue) → 失败 → 直接返回 任务完成 → finally设置isRunningfalse 请求4 → CAS检查(isRunningfalse) → 成功 → 设置isRunningtrue → 执行任务线程池方案的工作流程请求1 → 线程空闲 → 立即执行 请求2 → 线程忙碌 → 进入队列队列容量1→ 等待 请求3 → 线程忙碌 队列已满 → 触发DiscardPolicy → 直接丢弃 请求4 → 线程忙碌 队列已满 → 触发DiscardPolicy → 直接丢弃 任务完成 → 从队列取出请求2执行两种方案的差异维度CAS 方案线程池方案代码行数~15行~8行状态管理手动管理AtomicBoolean线程池自动管理容错性忘记释放会导致死锁无此风险缓冲能力无缓冲第2个请求就被拒绝允许1个请求排队可配置性需修改代码可通过参数调整可读性需要理解 CAS 机制标准的线程池用法维护成本高低为什么推荐线程池方案1. 职责单一原则线程池的核心职责就是管理任务和线程包括控制并发线程数管理任务队列处理任务拒绝使用 CAS 手动控制相当于绕过了线程池的任务管理机制。2. 更少的代码 更少的 Bug// CAS 方案需要记住在 finally 中释放try{doWork();}finally{isRunning.set(false);// ⚠️ 容易忘记}// 线程池方案无需手动管理singleWorker.submit(()-doWork());// ✅ 简洁明了3. 更好的扩展性如果需要调整行为只需修改线程池参数// 允许最多3个任务排队newArrayBlockingQueue(3)// 改为记录日志而不是丢弃newThreadPoolExecutor.CallerRunsPolicy()// 改为抛出异常newThreadPoolExecutor.AbortPolicy()4. 符合 Java 最佳实践《Effective Java》和《Java Concurrency in Practice》都推荐优先使用标准库提供的并发工具而不是手动实现同步逻辑。实际效果验证测试场景连续快速调用 5 次send()方法for(inti1;i5;i){sender.send();System.out.println(提交请求 i);Thread.sleep(100);}CAS 方案输出提交请求 1 → ✅ 获取执行权开始执行 提交请求 2 → ⚠️ 任务正在执行中跳过 提交请求 3 → ⚠️ 任务正在执行中跳过 提交请求 4 → ⚠️ 任务正在执行中跳过 提交请求 5 → ⚠️ 任务正在执行中跳过结果只有第1个请求被执行其余全部被拒绝。线程池方案输出提交请求 1 → ✅ 立即执行 提交请求 2 → 进入队列等待 提交请求 3 → ❌ 队列已满丢弃 提交请求 4 → ❌ 队列已满丢弃 提交请求 5 → ❌ 队列已满丢弃 任务1完成 → 从队列取出请求2执行结果第1个请求执行第2个请求排队其余被丢弃。注意事项1. 队列容量的选择// 队列容量 0完全不允许排队立即丢弃newArrayBlockingQueue(0)// 队列容量 1允许1个任务排队当前方案newArrayBlockingQueue(1)// 队列容量 N允许N个任务排队newArrayBlockingQueue(N)2. 拒绝策略的选择策略行为适用场景DiscardPolicy直接丢弃新任务任务可丢失如日志推送CallerRunsPolicy由调用线程执行不能丢失任务但要限流AbortPolicy抛出 RejectedExecutionException需要感知任务被拒绝DiscardOldestPolicy丢弃队列中最老的任务保证最新任务优先执行3. 资源清理记得在应用关闭时优雅地关闭线程池PreDestroypublicvoidshutdown(){singleWorker.shutdown();try{if(!singleWorker.awaitTermination(5,TimeUnit.SECONDS)){singleWorker.shutdownNow();}}catch(InterruptedExceptione){singleWorker.shutdownNow();Thread.currentThread().interrupt();}}总结核心观点优先使用标准库Java 并发包已经提供了丰富的工具不要重复造轮子简单即是美能用一行代码解决的问题不要用十行让专业的人做专业的事线程池负责管理任务我们只负责提交任务学习要点理解ThreadPoolExecutor的参数含义掌握不同拒绝策略的使用场景学会根据业务需求选择合适的队列容量养成阅读 JDK 源码的习惯了解标准库的实现原理参考资料《Effective Java》第3版 - 第81条优先使用并发工具类而不是 wait 和 notify《Java Concurrency in Practice》- 第8章线程池的使用Oracle 官方文档[ThreadPoolExecutor])