Java多线程作业队列避免饥饿的锁优化技术咨询
问题解答
问题1:拆分锁是否能避免addJob饥饿?
是的,拆分锁是解决该饥饿问题的合理方案。
原代码的核心问题在于runJob方法中,synchronized(jobq)块包裹了整个while循环——只要队列不为空,锁会一直被持有,直到队列被取空为止。哪怕exec.submit()是异步提交任务,锁也不会提前释放,导致addJob方法完全无法获取jobq锁来添加新任务,极端情况下会出现饥饿(addJob长期无法执行)。
拆分锁的思路是缩短锁的持有时间:仅在「检查队列是否有作业」和「取出作业」的临界操作期间持有jobq锁,而提交任务到线程池的操作(exec.submit())在锁外执行。这样,两次取作业的间隙,jobq锁会被释放,addJob就能获取锁添加新任务,不会被长时间阻塞。
问题2:用synchronized如何实现拆分锁?
关键是把「取作业」和「提交任务」的操作拆分,仅在取作业的临界步骤持有锁。修改后的代码示例如下:
class JobQueue { // 建议去掉static,除非确实需要全局共享队列 Queue<Job> jobq = new LinkedList<>(); ExecutorService exec; public JobQueue(ExecutorService exec) { this.exec = exec; } public void addJob(Job j) { synchronized(jobq) { jobq.add(j); } } public void runJob() { while (true) { Job job; // 仅在获取作业的瞬间持有锁 synchronized(jobq) { if (jobq.isEmpty()) { break; // 队列空时退出循环 } // 在锁内完成取出操作,避免竞态 job = jobq.poll(); } // 锁已释放,再提交任务到线程池 exec.submit(job::run); } } }
代码说明:
- 把原
while (!jobq.isEmpty())拆分为循环内的锁内检查:每次循环先短暂锁定jobq,检查队列是否为空,若不为空则取出一个作业,随后立即释放锁。 - 提交任务到线程池的操作(
exec.submit())在锁外执行,此时jobq锁已经释放,addJob可以正常获取锁添加新任务。 - 修正了原代码的语法问题(比如
addJob方法缺少大括号、jobq未初始化等),同时去掉了不必要的static修饰符(如果业务不需要全局队列的话)。
额外优化方案:
如果希望减少锁竞争次数,可以在锁内一次性取出一批作业,再批量提交:
public void runJob() { List<Job> batch = new ArrayList<>(); while (true) { synchronized(jobq) { if (jobq.isEmpty()) { break; } // 一次性取出最多10个作业 jobq.drainTo(batch, 10); } if (batch.isEmpty()) { break; } batch.forEach(job -> exec.submit(job::run)); batch.clear(); } }
内容的提问来源于stack exchange,提问作者Kevin
相关产品推荐
相关产品推荐

