AtomicBoolean + CAS 机制 关于 线程池任务丢弃策略优化

背景

在审查 GridItemPushSender 类时,发现同事使用 AtomicBoolean + CAS 机制来控制任务的串行执行。虽然功能正确,但存在更简洁的解决方案。

原始实现(CAS 方案)

代码示例

java 复制代码
public class GridItemPushSender {
    private final ExecutorService singleWorker;
    private final AtomicBoolean isRunning = new AtomicBoolean(false);
    
    public void send() {
        // CAS 尝试获取执行权
        if (!isRunning.compareAndSet(false, true)) {
            log.info("任务正在执行中,本次请求跳过");
            return;
        }
        
        singleWorker.submit(() -> {
            try {
                doSend();
            } catch (Exception e) {
                log.error("推送异常: {}", e.getMessage());
            } finally {
                // 释放执行权
                isRunning.set(false);
            }
        });
    }
}

问题分析

  1. 代码冗余 :需要手动管理 isRunning 标志位
  2. 容易出错 :如果忘记在 finally 中重置标志,会导致后续所有请求被拒绝
  3. 职责不清:线程池本身就有队列管理机制,重复造轮子
  4. 维护成本高:增加了额外的状态变量,提高了理解难度

优化方案(线程池内置机制)

核心思路

利用 ThreadPoolExecutor有界队列 + 拒绝策略来实现相同的功能,让线程池自己管理任务丢弃逻辑。

优化后代码

java 复制代码
public class GridItemPushSender {
    private final ExecutorService singleWorker;
    
    public GridItemPushSender(GridItemPushLogGateway pushLogGateway, ProgramGateway apiClient) {
        this.pushLogGateway = pushLogGateway;
        this.apiClient = apiClient;
        
        // 使用 ThreadPoolExecutor 自定义配置
        this.singleWorker = new ThreadPoolExecutor(
                1,                              // 核心线程数
                1,                              // 最大线程数
                0L, TimeUnit.MILLISECONDS,      // 空闲线程存活时间
                new ArrayBlockingQueue<>(1),    // 容量为1的有界队列
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy()  // 拒绝策略:直接丢弃
        );
    }
    
    public void send() {
        singleWorker.submit(() -> {
            try {
                doSend();
            } catch (Exception e) {
                log.error("推送异常: {}", e.getMessage());
            }
        });
    }
}

工作原理对比

CAS 方案的工作流程

复制代码
请求1 → CAS检查(isRunning=false) → 成功 → 设置isRunning=true → 执行任务
请求2 → CAS检查(isRunning=true)  → 失败 → 直接返回
请求3 → CAS检查(isRunning=true)  → 失败 → 直接返回
任务完成 → finally设置isRunning=false
请求4 → CAS检查(isRunning=false) → 成功 → 设置isRunning=true → 执行任务

线程池方案的工作流程

复制代码
请求1 → 线程空闲 → 立即执行
请求2 → 线程忙碌 → 进入队列(队列容量1)→ 等待
请求3 → 线程忙碌 + 队列已满 → 触发DiscardPolicy → 直接丢弃
请求4 → 线程忙碌 + 队列已满 → 触发DiscardPolicy → 直接丢弃
任务完成 → 从队列取出请求2执行

两种方案的差异

维度 CAS 方案 线程池方案
代码行数 ~15行 ~8行
状态管理 手动管理 AtomicBoolean 线程池自动管理
容错性 忘记释放会导致死锁 无此风险
缓冲能力 无缓冲,第2个请求就被拒绝 允许1个请求排队
可配置性 需修改代码 可通过参数调整
可读性 需要理解 CAS 机制 标准的线程池用法
维护成本

为什么推荐线程池方案?

1. 职责单一原则

线程池的核心职责就是管理任务和线程,包括:

  • 控制并发线程数
  • 管理任务队列
  • 处理任务拒绝

使用 CAS 手动控制相当于绕过了线程池的任务管理机制。

2. 更少的代码 = 更少的 Bug

java 复制代码
// CAS 方案:需要记住在 finally 中释放
try {
    doWork();
} finally {
    isRunning.set(false);  // ⚠️ 容易忘记
}

// 线程池方案:无需手动管理
singleWorker.submit(() -> doWork());  // ✅ 简洁明了

3. 更好的扩展性

如果需要调整行为,只需修改线程池参数:

java 复制代码
// 允许最多3个任务排队
new ArrayBlockingQueue<>(3)

// 改为记录日志而不是丢弃
new ThreadPoolExecutor.CallerRunsPolicy()

// 改为抛出异常
new ThreadPoolExecutor.AbortPolicy()

4. 符合 Java 最佳实践

《Effective Java》和《Java Concurrency in Practice》都推荐:

优先使用标准库提供的并发工具,而不是手动实现同步逻辑。

实际效果验证

测试场景

连续快速调用 5 次 send() 方法:

java 复制代码
for (int i = 1; i <= 5; 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. 队列容量的选择

java 复制代码
// 队列容量 = 0:完全不允许排队,立即丢弃
new ArrayBlockingQueue<>(0)

// 队列容量 = 1:允许1个任务排队(当前方案)
new ArrayBlockingQueue<>(1)

// 队列容量 = N:允许N个任务排队
new ArrayBlockingQueue<>(N)

2. 拒绝策略的选择

策略 行为 适用场景
DiscardPolicy 直接丢弃新任务 任务可丢失,如日志推送
CallerRunsPolicy 由调用线程执行 不能丢失任务,但要限流
AbortPolicy 抛出 RejectedExecutionException 需要感知任务被拒绝
DiscardOldestPolicy 丢弃队列中最老的任务 保证最新任务优先执行

3. 资源清理

记得在应用关闭时优雅地关闭线程池:

java 复制代码
@PreDestroy
public void shutdown() {
    singleWorker.shutdown();
    try {
        if (!singleWorker.awaitTermination(5, TimeUnit.SECONDS)) {
            singleWorker.shutdownNow();
        }
    } catch (InterruptedException e) {
        singleWorker.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

总结

核心观点

  1. 优先使用标准库:Java 并发包已经提供了丰富的工具,不要重复造轮子
  2. 简单即是美:能用一行代码解决的问题,不要用十行
  3. 让专业的人做专业的事:线程池负责管理任务,我们只负责提交任务

学习要点

  • 理解 ThreadPoolExecutor 的参数含义
  • 掌握不同拒绝策略的使用场景
  • 学会根据业务需求选择合适的队列容量
  • 养成阅读 JDK 源码的习惯,了解标准库的实现原理

参考资料:

  • 《Effective Java》第3版 - 第81条:优先使用并发工具类而不是 wait 和 notify
  • 《Java Concurrency in Practice》- 第8章:线程池的使用
  • Oracle 官方文档:ThreadPoolExecutor)
相关推荐
灯厂码农7 小时前
C语言动态内存分配完全指南(malloc、calloc、realloc、free)
java·c语言·算法
梦梦代码精8 小时前
电商系统不是技术堆叠:LikeShop如何用分层Hold住复杂业务?
java·docker·代码规范
负责的蛋挞9 小时前
异步HttpModule的实现方式
java·服务器·前端
AC赳赳老秦9 小时前
防火墙规则批量配置实战:OpenClaw 自动生成模板、批量下发与合规性校验全解析
java·开发语言·人工智能·python·github·php·openclaw
Tian_Hang9 小时前
Eclipse Ditto 物模型相关代码
java·运维·服务器·ide·eureka·eclipse
Mr-Wanter10 小时前
wsl2 jdk管理工具之sdkman
java·开发语言·sdkman
唐青枫11 小时前
Java Future 与 CompletableFuture 实战指南:从异步结果到任务编排
java
长孙豪翔11 小时前
在.net中读写config文件的各种方法
java·数据库·.net
tachibana211 小时前
hot100 回文链表(234)
java·网络·数据结构·leetcode·链表
可乐ea11 小时前
【Java八股|第10篇】Java 中的包装类和自动拆装箱
java·面试题·包装类·java八股