Java自定义Barrier实现遭遇死锁,请求技术分析与解决
嘿,我仔细看了你写的自定义Barrier实现,这个死锁问题其实是并发编程里很常见的竞态条件导致的,咱们一步步拆解问题,再给你修复方案:
死锁原因分析
你的代码里有几个核心问题直接导致了死锁:
- 非原子的计数操作:
counter++不是原子操作,它会被拆分成「读取当前值→加1→写回新值」三个步骤。当多个线程同时执行这个操作时,会出现竞态条件——比如两个线程同时读到counter=4,各自加1后写回,最终counter还是5,但实际上有两个线程都认为自己是最后一个到达的;更糟的情况是,多个线程的更新互相覆盖,导致counter永远达不到toWait的数值,最后所有线程都卡在barrier.acquire()上,没人释放信号量许可,直接死锁。 - 无同步保护的条件判断:即使你后续补全了
if (counter == toWait)的释放逻辑,因为counter的更新没有同步保护,这个判断也会出现线程安全问题,可能永远触发不了释放逻辑,或者重复触发(虽然Semaphore允许多次release,但这里主要问题是触发不了)。 - static变量的共享风险:虽然
barrier、counter、toWait用static是为了让所有Barrier实例共享,但没有同步机制的共享,只会放大线程安全问题。
修复后的实现方案
我给你两种修复后的版本,你可以根据需求选择:
方案一:用synchronized同步块保护计数逻辑
这种方式简单直观,适合入门理解:
public class Barrier implements Runnable { private static final Semaphore barrier = new Semaphore(0); private static final int toWait = 5; private static int counter = 0; private static long sleepTime; public static int ID = 0; private final int id = ++ID; public Barrier(long sleep) { sleepTime = sleep; } @Override public void run() { try { Thread.sleep(sleepTime); System.out.println("线程" + id + "到达屏障"); // 用类对象作为锁,保护counter的更新和条件判断 synchronized (Barrier.class) { counter++; // 只有最后一个到达的线程释放所有许可 if (counter == toWait) { barrier.release(toWait); System.out.println("最后一个线程到达,释放所有许可"); // 重置计数器,支持Barrier重复使用 counter = 0; } } // 等待屏障放行 barrier.acquire(); System.out.println("线程" + id + "通过屏障"); } catch (InterruptedException e) { // 重新设置中断状态,避免中断信号丢失 Thread.currentThread().interrupt(); e.printStackTrace(); } } }
方案二:用AtomicInteger实现原子计数(更高效)
如果追求更高的性能,可以用JUC包的原子类来避免锁竞争:
import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; public class Barrier implements Runnable { private static final Semaphore barrier = new Semaphore(0); private static final int toWait = 5; private static final AtomicInteger counter = new AtomicInteger(0); private static long sleepTime; public static int ID = 0; private final int id = ++ID; public Barrier(long sleep) { sleepTime = sleep; } @Override public void run() { try { Thread.sleep(sleepTime); System.out.println("线程" + id + "到达屏障"); // 原子递增计数,返回递增前的旧值 int currentCount = counter.getAndIncrement(); // 当旧值等于toWait-1时,说明当前线程是最后一个到达的 if (currentCount == toWait - 1) { barrier.release(toWait); System.out.println("最后一个线程到达,释放所有许可"); // 重置计数器,支持重复使用 counter.set(0); } // 等待屏障放行 barrier.acquire(); System.out.println("线程" + id + "通过屏障"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } } }
额外提示
- 如果你希望这个Barrier可以重复使用,一定要在释放许可后重置计数器(上面的代码已经做了)。
sleepTime是static的,如果你想让不同的Barrier实例有不同的休眠时间,应该把它改成非static成员变量,不然后面创建的实例会覆盖前面的sleepTime值。- 处理
InterruptedException时,重新设置线程的中断状态是个好习惯,避免中断信号被吞掉导致后续逻辑异常。
内容的提问来源于stack exchange,提问作者Rango
相关产品推荐
相关产品推荐

