Java线程池队列本身不支持自动去重,需二次封装:用ConcurrentHashMap缓存任务指纹实现“有则拒收、无则入队”,配合带过期机制的本地缓存与执行后主动清理,确保线程安全、低延迟且兼容ThreadPoolExecutor。

Java中不能靠线程池队列本身自动去重,必须通过二次封装,在任务入队前做唯一性校验。核心思路是:用一个线程安全的集合(如ConcurrentHashMap)记录已提交任务的标识,结合阻塞队列实现“有则拒收、无则入队”的控制逻辑。
为什么原生BlockingQueue不支持去重
ArrayBlockingQueue、LinkedBlockingQueue等原生实现只负责存储和顺序调度,不感知元素语义。即使任务内容完全相同,只要对象引用不同或未重写equals/hashCode,队列就视为不同元素。直接调用offer()或put()无法拦截重复提交。
封装UniqueTaskQueue的关键设计
需同时满足三个条件:线程安全、低延迟判断、与ThreadPoolExecutor兼容。推荐采用以下结构:
Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
- 底层使用LinkedBlockingQueue或ArrayBlockingQueue作为实际任务容器
- 配套使用ConcurrentHashMap<String, Boolean>缓存任务指纹(如userId:skuId:timestamp的MD5或拼接key)
- 提供带去重语义的offerWithDedup(Runnable task, String dedupKey)方法
- dedupKey生成必须稳定——建议由业务层统一计算,避免在队列内做复杂解析
防止误判与状态清理
去重不是永久性屏蔽,而是限定时间窗口内的幂等控制。否则长期运行会导致内存泄漏或漏处理合法请求:
立即学习“Java免费学习笔记(深入)”;
- 不依赖Set.clear()全局清空,而应使用带有过期机制的缓存,例如Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.SECONDS)
- 若任务执行失败且需重试,重试时应生成新dedupKey(如加入重试序号),否则会被当作重复拒绝
- 在任务run()执行完毕后,由执行线程主动调用removeFromDedupCache(dedupKey),确保资源及时释放
与线程池协同使用的注意事项
封装后的队列需无缝接入ThreadPoolExecutor,重点注意两点:
- 构造线程池时,传入的是你封装类的内部queue字段,而非整个封装对象
- 拒绝策略(RejectedExecutionHandler)中若需落库或发MQ,也应基于原始dedupKey操作,而不是重新解析Runnable内容
- 避免在offerWithDedup中做耗时操作(如远程调用、DB查询),所有校验必须在内存中完成

















