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
- 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.
- Use java.util.concurrent.CountDownLatch instead, which handles this atomically and is battle-tested.
- 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
- Prefer java.util.concurrent.CountDownLatch over this class — it handles concurrent countDown atomically.
- Ensure the total number of countDown() calls exactly matches the initial count passed to the constructor.
- Do not reuse a latch after it has reached zero; create a new instance for the next batch.
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.