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

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











