crossoverJie/JCSprout · error · RuntimeException

concurrent error

Error message

concurrent error

What it means

Thrown in countDown() when the AtomicInteger counter goes below zero after decrementAndGet(). This is a check-then-act race: when counter is 1, two concurrent threads can both pass the counter.get() <= 0 guard (it reads 1, which is > 0), then both decrement — one reaches 0, the other reaches -1 and triggers the error. The guard is not atomic with the decrement.

Source

Thrown at src/main/java/com/crossoverjie/concurrent/communication/MultipleThreadCountDownKit.java:54

     * @param notify
     */
    public void setNotify(Notify notify){
        notifyListen = notify ;
    }


    /**
     * 线程完成后计数 -1
     */
    public void countDown(){

        if (counter.get() <= 0){
            return;
        }

        int count = this.counter.decrementAndGet();
        if (count < 0){
            throw new RuntimeException("concurrent error") ;
        }

        if (count == 0){
            synchronized (notify){
                notify.notify();
            }
        }

    }

    /**
     * 等待所有的线程完成
     * @throws InterruptedException
     */
    public void await() throws InterruptedException {
        synchronized (notify){
            while (counter.get() > 0){
                notify.wait();

View on GitHub (pinned to fc4c6e5f6d)

Solutions

  1. Replace the check-then-decrement with an atomic CAS loop: spin on compareAndSet until you decrement from a positive value, returning early if the value is already 0.
  2. Use java.util.concurrent.CountDownLatch instead, which handles this atomically and is battle-tested.
  3. If you must keep this class, ensure the number of countDown() calls never exceeds the initial count by auditing all call sites.

Example fix

// before (race between get() and decrementAndGet())
if (counter.get() <= 0) { return; }
int count = counter.decrementAndGet();
if (count < 0) { throw new RuntimeException("concurrent error"); }

// after (atomic CAS loop — no window)
int current;
do {
    current = counter.get();
    if (current <= 0) { return; }
} while (!counter.compareAndSet(current, current - 1));
if (current - 1 == 0) {
    synchronized (notify) { notify.notify(); }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the number of countDown() calls never exceeds the initial count.
// Track calls yourself if the calling code is complex:
AtomicInteger downCalls = new AtomicInteger(0);
void safeCountDown(MultipleThreadCountDownKit latch, int initial) {
    if (downCalls.incrementAndGet() > initial) {
        LOGGER.warn("countDown called more times than initial count");
        return;
    }
    latch.countDown();
}

Try / catch

try {
    latch.countDown();
} catch (RuntimeException e) {
    if ("concurrent error".equals(e.getMessage())) {
        // Lost a race in the non-atomic check-then-decrement; safe to ignore
        // since the latch has already reached zero.
        LOGGER.debug("countDown race: latch already at zero", e);
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Two or more threads call countDown() simultaneously when the counter is at 1 (or generally when more threads call countDown than the remaining count). The get() check and the decrementAndGet() are separate operations with a window between them.

Common situations: High-convergence point in a parallel pipeline where multiple workers finish at nearly the same instant. Reusing the same latch object for a second wave of countDown calls after it already reached zero. Counting down more times than the initial count due to a logic error in the caller.

Related errors


AI-assisted analysis of crossoverJie/JCSprout@fc4c6e5f6d (2026-08-14). Data as JSON: /api/errors/a400ed082af9574d. Report an issue: GitHub.