带唯一元素的线程安全FIFO队列配合线程池实现调度文件复制的死锁修复问题
问题根因分析
现有代码死锁的核心原因是 worker 线程在持有全局锁 scheduledReplicationsLock 的情况下调用了 BlockingQueue.take(),队列为空时线程会一直阻塞在该方法上,且全程不释放锁,导致调度线程执行 add 方法时永远拿不到锁,无法添加新任务。
关于poll+休眠方案的评价
不推荐使用poll()加休眠的替代方案,这种方案属于折中妥协的dirty fix:休眠间隔设置过短会导致CPU空转浪费资源,设置过长会导致任务触发延迟,完全没有必要。
现有代码最小改动修复方案
你之前担心的「把take()移到同步块外会导致状态非原子更新」的问题实际不存在,核心逻辑是:
所有入队操作都是在持有全局锁的情况下原子完成「加入队列+ID写入queuedReplicationIds」的,只要任务还没有被worker线程更新状态,ID就一定留在queuedReplicationIds中,调度线程的重复提交判断逻辑不会失效。
修改后的worker线程run方法如下:
public void run() { while (true) { Replication replication = null; try { // 不持有锁的情况下阻塞等待任务,不会阻塞调度线程加任务 replication = replicationQueue.take(); } catch (InterruptedException e) { // 处理中断逻辑,比如退出线程 Thread.currentThread().interrupt(); break; } Long replicationId = replication.getId(); synchronized (scheduledReplicationsLock) { // 把ID从待执行集合移到运行中集合 queuedReplicationIds.remove(replicationId); runningReplicationIds.add(replicationId); } try { executeReplication(replication); } finally { // 【原来的代码漏了这个关键逻辑!】任务执行完成后必须移除运行中ID synchronized (scheduledReplicationsLock) { runningReplicationIds.remove(replicationId); } } } }
同时补充add方法遗漏的返回值:
public boolean add(Replication replication) { synchronized (scheduledReplicationsLock) { if (queuedReplicationIds.contains(replication.id) || runningReplicationIds.contains(replication.id)) { return false; } replicationQueue.add(replication); queuedReplicationIds.add(replication.id); return true; // 补充遗漏的返回 } }
优化方案参考
如果想要进一步简化实现,可以做如下优化:
- 合并两个ID集合为一个
Map<Long, TaskState>,TaskState是枚举类:QUEUED/RUNNING,减少集合操作的冗余代码 - 如果使用JDK并发包的
ThreadPoolExecutor,可以重写beforeExecute和afterExecute方法来统一处理任务状态的更新,不用自己实现worker线程逻辑 - 队列规模较小的场景下,不需要额外维护
queuedReplicationIds,可以直接遍历队列判断ID是否存在,减少维护成本
内容的提问来源于stack exchange,提问作者Alan Evangelista
相关产品推荐
相关产品推荐

