Real-world examples demonstrating common Java concurrency bugs that @AsyncTest finds but standard @Test misses.
Most examples ship the buggy service, a sequential @Test that passes on it, and an
@AsyncTest that exposes the bug. Those @AsyncTest demonstrations are @Disabled: 96 of
the 151 examples have one. That is deliberate: they demonstrate code that fails, so enabling
them would make the examples pipeline permanently red. Each carries a reason saying so, and
removing the annotation is the intended way to watch the bug surface.
The newer examples take a different shape, and it is the better one. Rather than a disabled
demonstration, they drive the detector's own recording API from ordinary @Test methods and
assert on the report: clean usage stays silent, the buggy pattern is flagged, and the severity
is pinned. Those tests run in CI, so they prove the detector still behaves as documented
rather than only that the example compiles. Forty-one examples are written this way, and every
example from 136 onwards is.
The distinction matters when reading the pipeline's green tick. For the disabled majority, the
examples pipeline builds all 151 and runs their enabled tests, so it proves those examples
compile and keep working against the current library — it does not prove any detector
fires. The check that does that for the library itself is DetectionCoverageTest in
async-test-lib, which runs real buggy code through a real @AsyncTest and asserts on the
detector reporting channel.
The other thing these examples make concrete is how a detector gets its data. Only a few, such as
DeadlockDetector, observe the JVM directly and need nothing from you. Most need the test body to
tell them what happened, which is why you will see calls like
AsyncTestContext.sharedCollectionDetector().recordWrite(events, "event-log", "add");in the disabled demonstrations. The alternative to writing those by hand is the agent, which weaves JavaBean accessors and feeds the detectors for you, see ../docs/AGENT.md.
| # | Example | Primary Detector | Async Problem | Severity |
|---|---|---|---|---|
| 01 | CompletableFuture Exception Handling | CompletableFutureExceptionDetector |
Unhandled exceptions in async chains cause silent data loss | 🔴 Critical |
| 02 | Visibility/Volatile Flag | VisibilityMonitor |
Missing volatile on shared flags causes threads to never see shutdown signals |
🔴 Critical |
| 03 | Shared Non-Thread-Safe Collection | SharedCollectionDetector |
ArrayList/HashMap shared across threads causes data loss and corruption | 🔴 Critical |
| 04 | Virtual Thread Context Leak | VirtualThreadContextLeakDetector |
ThreadLocal leaks in virtual threads cause memory leaks | 🟡 Medium |
| 05 | Lock Contention | LockContentionDetector |
Many threads competing for one monitor — wait time dominates, throughput collapses before the CPUs are busy | 🟡 High |
| 06 | Deadlock | DeadlockDetector |
Two threads acquire the same pair of locks in opposite order — circular wait, neither can proceed | 🔴 Critical |
| 07 | Livelock | LivelockDetector (reports a round that timed out with the spin still running) |
Two nodes back off and retry without delay — threads stay active but make no progress | 🔴 Critical |
| 08 | Race Condition | RaceConditionDetector |
Non-atomic check-then-update lets multiple threads pass the stock threshold, driving count negative | 🔴 Critical |
| 10 | Shared Non-Thread-Safe Types | SharedMatcherDetector, SharedDecimalFormatDetector, SharedMessageDigestDetector |
Shared Matcher, DecimalFormat, and MessageDigest fields silently produce wrong results under concurrent load |
🔴 Critical |
| 11 | Interrupt Swallowing | InterruptSwallowingDetector |
catch(InterruptedException) without restoring the flag permanently suppresses cooperative cancellation |
🔴 Critical |
| 12 | MDC Context Leak | MdcContextLeakDetector |
MDC entries not cleared at task end contaminate the next request on the reused thread | 🟡 High |
| 13 | System Property Mutation | SystemPropertyMutationDetector |
Concurrent System.setProperty() causes non-deterministic configuration and test pollution |
🟡 High |
| 14 | Future Ignored | FutureIgnoredDetector |
submit() result never inspected — task exceptions silently swallowed |
🔴 Critical |
| 15 | Explicit GC | ExplicitGcDetector |
System.gc() triggers unpredictable STW pauses that corrupt concurrency timing tests |
🟡 High |
| 16 | Deprecated Thread API | DeprecatedThreadApiDetector |
Thread.stop()/suspend()/resume() are unsafe and removed in Java 20+ |
🔴 Critical |
| 17 | Shared XML Parser | SharedXmlParserDetector |
DocumentBuilder/Transformer shared across threads causes corrupted parse results |
🔴 Critical |
| 18 | Boxed Primitive Lock | BoxedPrimitiveLockDetector |
synchronized on cached Integer/Boolean acquires a JVM-global shared monitor |
🔴 Critical |
| 19 | Shared TimeZone | SharedTimeZoneDetector |
TimeZone.setRawOffset() from multiple threads produces silently wrong date/time arithmetic |
🟡 High |
| 20 | Uncaught Exception Handler | UncaughtExceptionHandlerDetector |
Threads without a custom UncaughtExceptionHandler discard thrown exceptions silently |
🟡 High |
| 21 | Busy Wait | BusyWaitDetector |
Tight spin loop polling an empty queue wastes CPU and prevents other threads from running | 🟡 High |
| 22 | Atomicity Violation | AtomicityValidator |
Non-atomic read-modify-write on a volatile long loses increments under concurrent load |
🔴 Critical |
| 23 | ThreadLocal Leak | ThreadLocalMonitor |
Request context stored in ThreadLocal never cleared — next task on the reused thread sees stale auth data |
🟡 High |
| 24 | Interrupt Mishandling | InterruptMonitor |
catch(InterruptedException) without restoring the interrupt flag breaks cooperative cancellation |
🔴 Critical |
| 25 | Executor Self-Deadlock | ExecutorDeadlockDetector |
Task on a single-thread pool submits a subtask to the same pool and blocks waiting — the subtask never runs | 🔴 Critical |
| 26 | Future Blocking | FutureBlockingDetector |
Pool workers call Future.get() on futures submitted to the same bounded pool — starvation when all threads block |
🔴 Critical |
| 27 | Latch Misuse | LatchMisuseDetector |
countDown() in both catch and finally blocks causes premature latch completion and races to the next phase |
🟡 High |
| 28 | Unsafe Lazy Init | LazyInitValidator (standalone, not a DetectorType) |
Double-checked locking without volatile — partially-constructed singleton visible to other threads |
🔴 Critical |
| 29 | ABA Problem | ABAProblemDetector |
Lock-free stack CAS succeeds despite node being recycled — classic A→B→A problem corrupts the stack | 🔴 Critical |
| 30 | False Sharing | FalseSharingDetector (experimental, findings opt-in) |
Adjacent volatile long fields on the same cache line cause coherence traffic between threads updating independent counters |
🟡 High |
| 31 | Lock Order Violation | LockOrderValidator |
Transfer service acquires account locks in source-first order — two concurrent opposite transfers create a circular dependency | 🔴 Critical |
| 32 | RW Lock Starvation | ReadWriteLockMonitor |
Non-fair ReentrantReadWriteLock(false) lets readers cut ahead of waiting writers indefinitely — writes never complete |
🟡 High |
| 33 | 1.5.0/1.6.0 Feature Tour | (tour, not a bug) | Runnable tour of the public API: Preset, threadCounts, replaySeed, awaitAsync, scoped listeners, MarkdownFormatter |
🟢 Low |
| 34 | Blocking Queue | BlockingQueueDetector |
Fire-and-forget offer() holds a bounded queue at capacity and discards the rejected items; poll() NPE on null return |
🟡 High |
| 35 | Calendar Misuse | CalendarDetector |
Shared Calendar instance mutated concurrently produces wrong date conversions |
🔴 Critical |
| 36 | Cache Concurrency | CacheConcurrencyDetector |
Plain HashMap used as a concurrent cache causes data loss and ConcurrentModificationException |
🔴 Critical |
| 37 | CF Chain Issues | CompletableFutureChainDetector |
Fire-and-forget CompletableFuture chains swallow exceptions silently |
🟡 High |
| 38 | CF Common-Pool Blocking | CompletableFutureCommonPoolBlockingDetector |
supplyAsync() + blocking .get() inside the same common pool starves it |
🔴 Critical |
| 39 | CF Completion Leak | CompletableFutureCompletionLeakDetector |
CompletableFuture created but complete() never called — waiting threads block forever |
🟡 High |
| 40 | ConcurrentMap Recursion | ConcurrentMapComputeRecursionDetector |
computeIfAbsent lambda calls computeIfAbsent on the same map — recursive deadlock |
🔴 Critical |
| 41 | Concurrent Modification | ConcurrentModificationDetector |
ArrayList iterated while another thread calls add() — ConcurrentModificationException |
🔴 Critical |
| 42 | Condition Variable | ConditionVariableDetector |
put() signals the wrong condition, stranding a consumer that was already waiting |
🟡 High |
| 43 | Copy-On-Write Misuse | CopyOnWriteCollectionDetector |
CopyOnWriteArrayList on a write-heavy path — O(n) copy per write degrades throughput |
🟡 High |
| 44 | CountDownLatch Misuse | CountDownLatchDetector |
countDown() skipped in one code path — await() blocks forever |
🔴 Critical |
| 45 | CyclicBarrier Broken | CyclicBarrierDetector |
A stranded worker's timed-out await() breaks the barrier, nobody resets it, and later phases arrive at it broken |
🟡 High |
| 46 | Daemon Thread Hygiene | DaemonThreadHygieneDetector |
Non-daemon background threads prevent JVM from shutting down | 🟢 Low |
| 47 | Double-Checked Locking | DoubleCheckedLockingDetector |
DCL without volatile — partially constructed singleton visible to other threads |
🔴 Critical |
| 48 | Exchanger Misuse | ExchangerDetector |
Odd-numbered callers leave one thread blocked in exchange() without a partner |
🟡 High |
| 49 | Executor Shutdown Leak | ExecutorShutdownDetector |
ExecutorService created but shutdown() never called — threads leak per test run |
🟡 High |
| 50 | Fork Without Join | ForkJoinPoolDetector |
A forked half is never joined, so a sorted list comes back missing elements and a task's exception is lost | 🟡 High |
| 51 | ForkJoin Task Blocking | ForkJoinTaskBlockingDetector |
Thread.sleep() inside RecursiveTask.compute() pins ForkJoin worker thread |
🟡 High |
| 52 | HTTP Client Concurrency | HttpClientConcurrencyDetector |
sendAsync() whose future is discarded: the request is sent and nothing ever establishes whether it worked, on a client built per call |
🟢 Low |
| 53 | InheritableThreadLocal Misuse | InheritableThreadLocalMisuseDetector |
Thread pools inherit stale context from previous requests via InheritableThreadLocal |
🟡 High |
| 54 | JDBC Connection Shared | JdbcConnectionSharedDetector |
Single Connection shared across threads — concurrent queries corrupt each other |
🔴 Critical |
| 55 | Lazy Init Race | LazyInitRaceDetector |
Unsynchronized null-check lazy init — multiple threads create separate instances | 🔴 Critical |
| 56 | Lock Downgrade | LockDowngradeDetector |
Write lock released before read lock acquired, so another thread can write in the gap. Reported when a writer is observed inside the gap; the shape alone is also correct code (#355) | 🔴 Critical |
| 57 | Lock Leak | LockLeakDetector |
Exception between lock() and unlock() leaves lock permanently held |
🔴 Critical |
| 58 | Missed Signal | MissedSignalDetector |
notify() fired before wait() — signal lost, waiter blocks forever |
🔴 Critical |
| 59 | Mutable Map Key | MutableMapKeyDetector |
Mutable object used as HashMap key; mutation after insertion makes entry unreachable |
🟡 High |
| 60 | Nested Monitor Lockout | NestedMonitorLockoutDetector |
Thread holds monitor A and calls wait() on B; another holds B and notifies A — circular deadlock |
🔴 Critical |
| 61 | Notify Without Monitor | NotifyWithoutMonitorDetector |
notify() called without holding the object's monitor — IllegalMonitorStateException |
🔴 Critical |
| 62 | Optimistic Read Validation | OptimisticReadValidationDetector |
StampedLock.tryOptimisticRead() used without validate() — stale data returned silently |
🟡 High |
| 63 | Parallel Stream Side-Effects | ParallelStreamDetector |
parallelStream().forEach() mutates shared ArrayList — lost updates and CME |
🔴 Critical |
| 64 | Phaser Misuse | PhaserDetector |
Fewer parties registered than threads that arrive — a phase that never advances | 🟡 High |
| 65 | Public Lock Exposure | PublicLockExposureDetector |
synchronized methods make the object its own lock, and the object is public API, so external callers can hold it indefinitely |
🟡 High |
| 66 | Reentrant Lock Imbalance | ReentrantLockDetector |
lock() called twice, unlock() once — hold count stays at 1, starving all callers |
🔴 Critical |
| 67 | Resource Leak | ResourceLeakDetector |
InputStream opened per call but never closed — file descriptor exhaustion |
🟡 High |
| 68 | Scheduled Executor Leak | ScheduledExecutorDetector |
ScheduledExecutorService created but shutdown() never called — threads accumulate |
🟡 High |
| 69 | Scoped Value Misuse | ScopedValueMisuseDetector |
Context read without null-guard outside binding scope — NPE or wrong-user processing | 🟡 High |
| 70 | Semaphore Misuse | SemaphoreMisuseDetector |
release() not in finally — exception permanently drains semaphore permits |
🔴 Critical |
| 71 | Shared Formatter | SharedFormatterDetector |
Shared Formatter instance concurrent calls produce garbled output |
🟡 High |
| 72 | Shared Random | SharedRandomDetector |
Static Random shared across threads — seed contention degrades performance |
🟢 Low |
| 73 | SimpleDateFormat Shared | SimpleDateFormatDetector |
Static SimpleDateFormat concurrent format()/parse() corrupts internal calendar state |
🔴 Critical |
| 74 | Sleep In Lock | SleepInLockDetector |
Thread.sleep() inside synchronized block serializes all callers for the sleep duration |
🟡 High |
| 75 | StampedLock Not Unlocked | StampedLockDetector |
writeLock() stamp not released in finally — lock permanently held, all callers block |
🔴 Critical |
| 76 | Stateful Lambda | StatefulLambdaDetector |
Shared int[] captured by lambda mutated by multiple threads — non-atomic lost updates |
🔴 Critical |
| 77 | Stream Not Closed | StreamClosingDetector |
Stream backed by I/O resource opened but close() never called — file descriptor leak |
🟡 High |
| 78 | StringBuilder Shared | StringBuilderDetector |
Single StringBuilder appended concurrently — garbled output or ArrayIndexOutOfBoundsException |
🔴 Critical |
| 79 | Structured Concurrency Leak | StructuredConcurrencyMisuseDetector |
Executor (scope) forked but shutdown() never called — threads and resources leak |
🟡 High |
| 80 | SynchronizedList Iteration | SynchronizedCollectionIterationDetector |
synchronizedList iterated without external lock — iterator not thread-safe |
🔴 Critical |
| 81 | Synchronized Non-Final | SynchronizedNonFinalDetector |
Lock object is non-final and reassigned — two threads enter the critical section simultaneously | 🔴 Critical |
| 82 | Synchronized On Literal | SynchronizedOnLiteralDetector |
synchronized("string-literal") uses a JVM-global interned monitor — unintended contention |
🟡 High |
| 83 | Thread Factory Missing | ThreadFactoryDetector |
newFixedThreadPool without ThreadFactory — generic names, non-daemon, hard to debug |
🟢 Low |
| 84 | Thread Leak | ThreadLeakDetector |
New Thread spawned per connection, shutdown() never called — idle threads accumulate |
🟡 High |
| 85 | ThreadLocal Contamination | ThreadLocalContaminationDetector |
ThreadLocal.remove() not called — pooled thread carries previous request's context |
🟡 High |
| 86 | Thread Pool Deadlock | ThreadPoolDeadlockDetector |
Task blocks waiting for another task on the same bounded pool — pool exhaustion deadlock | 🔴 Critical |
| 87 | Thread Starvation | ThreadStarvationDetector |
Non-fair lock held by high-priority tasks — low-priority threads never scheduled | 🟡 High |
| 88 | Timer Misuse | TimerDetector |
java.util.Timer never cancelled; slow task delays all subsequent scheduled tasks |
🟢 Low |
| 89 | Unbounded Queue | UnboundedQueueDetector |
newFixedThreadPool backed by unbounded LinkedBlockingQueue — OOM under load |
🟡 High |
| 90 | VT Carrier Exhaustion | VirtualThreadCarrierExhaustionDetector |
synchronized + Thread.sleep() in virtual thread pins carrier, exhausting the pool |
🔴 Critical |
| 91 | VT CPU-Bound Task | VirtualThreadCpuBoundTaskDetector |
CPU-intensive loop on virtual thread monopolises carrier, defeating work-stealing | 🟡 High |
| 92 | VT Pinning | VirtualThreadPinningDetector |
synchronized method with I/O inside pins virtual thread carrier — defeats Loom's purpose |
🔴 Critical |
| 93 | Volatile Array | VolatileArrayDetector |
volatile int[] only guarantees reference visibility, not element visibility |
🟡 High |
| 94 | Wait Without Timeout | WaitTimeoutDetector |
wait() with no timeout — missed notify() causes permanent thread block |
🔴 Critical |
| 95 | Spurious Wakeup | WakeupDetector |
wait() guarded by if not while — spurious wakeup proceeds on unmet condition |
🟡 High |
| 96 | WeakHashMap Shared | WeakHashMapSharedDetector |
WeakHashMap shared across threads — GC-triggered cleanup + concurrent puts corrupt structure |
🔴 Critical |
| 97 | WeakReference Race | WeakReferenceRaceDetector |
ref.get() != null check then ref.get().use() — GC can clear between the two calls |
🟡 High |
| 98 | Async Pipeline Monitor | PipelineMonitor |
Uncoordinated pipeline stages with no back-pressure — slow stages lose events under load | 🟡 High |
| 99 | Atomic Non-Atomic Update | AtomicNonAtomicUpdateDetector |
counter.set(counter.get() + 1) on AtomicInteger — non-atomic compound op loses updates |
🔴 Critical |
| 100 | Constructor Safety | ConstructorSafetyValidator |
this published to registry before fields initialised — partially constructed object visible |
🔴 Critical |
| 101 | Memory Ordering | MemoryOrderingMonitor |
Non-volatile ready + value fields — consumer may read stale values |
🟡 High |
| 102 | Synchronizer Monitor | SynchronizerMonitor |
Three synchronizers (Semaphore + Lock + Latch) chained on one path — over-synchronised | 🟢 Low |
| 103 | Thread Pool Monitor | ThreadPoolMonitor |
2-thread pool saturated by 100 concurrent tasks — queue depth and rejection rate reported | 🟡 High |
| 104 | Shared Stateful Crypto | SharedStatefulCryptoDetector |
A Cipher/Mac/Signature shared across threads — interleaved init/update/doFinal corrupts ciphertext or breaks MAC/signature integrity |
🔴 Critical |
| 105 | Concurrent Map Check-Then-Act | NonAtomicConcurrentMapUpdateDetector |
containsKey-then-put on a ConcurrentMap — compound op is not atomic, concurrent callers lose updates |
🔴 Critical |
| 106 | Shared Deflater/Inflater | SharedDeflaterDetector |
A java.util.zip.Deflater shared across threads — stateful native zlib stream corrupts output or crashes on end() mid-stream |
🔴 Critical |
| 107 | This-Escape From Constructor | ThisEscapeDetector |
Constructor publishes this (registers a listener / starts a thread) before returning — other threads see a partially-constructed object |
🟡 High |
| 108 | ThreadLocalRandom Misuse | ThreadLocalRandomMisuseDetector |
A cached ThreadLocalRandom.current() reference used from other threads — defeats per-thread isolation, biases output |
🟡 High |
| 109 | CompletableFuture Obtrude Abuse | CompletableFutureObtrudeDetector |
obtrudeValue/obtrudeException overwrite an already-published result — downstream stages observe two different values for one future |
🟡 High |
| 110 | Spurious Wakeup Hazard | SpuriousWakeupDetector |
wait() outside a condition-checking loop — a wakeup is not a guarantee that the condition holds |
🔴 Critical |
| 111 | Lock Upgrade Deadlock | LockUpgradeDeadlockDetector |
Acquiring the write lock while holding the read lock — ReentrantReadWriteLock cannot upgrade, so the thread waits on itself |
🔴 Critical |
| 112 | tryLock Misuse | TryLockMisuseDetector |
unlock() after a tryLock() that failed, or a tryLock() whose result is never checked — releases a lock this thread never held |
🔴 Critical |
| 113 | CF Blocking Callback | CompletableFutureBlockingCallbackDetector |
A blocking call inside a thenApply/thenAccept callback occupies a common-pool worker for the duration |
🟡 High |
| 114 | StableValue Misuse | StableValueMisuseDetector (standalone, JDK 25/26) |
StableValue (JEP 502) read before set (NoSuchElementException) or set twice (lost update) |
🔴 Critical |
| 115 | StructuredTaskScope Misuse | StructuredTaskScopeMisuseDetector (standalone, JDK 25/26) |
StructuredTaskScope.open(Joiner) (JEP 505) lifecycle broken — fork-after-join, result-before-join, owner-confinement, missing join |
🔴 Critical |
| 116 | Gatherer Parallel Misuse | GathererConcurrencyMisuseDetector (standalone, JDK 24+) |
Stateful Gatherer (JEP 485) state shared across parallel segments; a gatherer with no combiner on a parallel stream (runs sequentially, LOW) |
🟠 High |
| 117 | LazyConstant Misuse | LazyConstantMisuseDetector (JDK 26) |
LazyConstant (Lazy Constants, 2nd preview) supplier returns null (NPE), re-enters itself, or runs more than once in a hand-rolled holder |
🔴 Critical |
| 118 | Final Field Mutation | FinalFieldMutationDetector (JEP 500, JDK 26) |
Reflective Field.set on a final field — warned on JDK 26, denied in a future release, voids the JMM final-field publication guarantee today |
🔴 Critical |
| 119 | Shared KDF | SharedKdfDetector (JEP 510, JDK 25) |
One javax.crypto.KDF instance shared across threads — documented not thread-safe, silently derives wrong keys |
🟡 High |
| 120 | Shared ByteBuffer | SharedByteBufferDetector |
One ByteBuffer framing every thread's message — position/limit/mark are mutable state, so relative put/get, flip() and clear() corrupt the cursor |
🔴 Critical |
| 121 | Shared CharsetEncoder/Decoder | SharedCharsetCoderDetector |
Charset is thread-safe, its coders are not — a cached CharsetEncoder garbles output or throws when two threads run its reset/encode/flush protocol |
🔴 Critical |
| 122 | Shared Checksum | SharedChecksumDetector |
One CRC32 accumulating across threads — produces a well-formed checksum over both payloads that matches neither, with no exception |
🔴 Critical |
| 123 | FileChannel Position Race | FileChannelPositionRaceDetector |
position(n) then read(buf) on one shared channel: another thread's call lands between the seek and the read, which returns the wrong record |
🔴 Critical |
| 124 | Shared Iterator | SharedIteratorDetector |
One Iterator used as a work queue — hasNext()/next() is check-then-act, so elements are skipped, doubled, or NoSuchElementException fires |
🔴 Critical |
| 125 | High-Contention Atomic | HighContentionAtomicDetector |
One hot AtomicLong — correct, but CAS-retry bound; advisory pointing at LongAdder for counters that are only ever totalled |
🟢 Low |
| 126 | Shared JSON Mapper Reconfig | SharedJsonMapperReconfigDetector |
Sharing the mapper is recommended; reconfiguring it after other threads are serializing through it drops caches mid-flight | 🔴 Critical |
| 127 | Shared SecureRandom | SharedSecureRandomDetector |
One SecureRandom behind every token — thread safety is provider-dependent, and a provider that does not synchronize can issue duplicate session tokens |
🔴 Critical |
| 128 | Kotlin Lost Update | RaceConditionDetector |
Kotlin's var compiles to the same non-atomic read-modify-write Java's does — count++ in a coroutine loses updates exactly as it would on a JVM thread |
🔴 Critical |
| 129 | Confined Arena Thread Escape | ConfinedArenaThreadEscapeDetector |
A segment from Arena.ofConfined() is handed to a pool thread — WrongThreadException if you are lucky, a read of freed memory after the arena closes if you are not |
🔴 Critical |
| 130 | Shared MemorySegment Race | SharedMemorySegmentRaceDetector |
Arena.ofShared() permits every thread to access the segment; it does not stop two of them tearing the same bytes |
🔴 Critical |
| 131 | VarHandle Non-Atomic Update | VarHandleNonAtomicUpdateDetector |
getVolatile then setVolatile reads as atomic and is two operations — volatile buys visibility, never atomicity of a read-modify-write |
🔴 Critical |
| 132 | Static Initializer Deadlock | StaticInitDeadlockDetector |
Two <clinit> blocks referencing each other deadlock on class-init locks, which are not monitors and so are invisible to ThreadMXBean |
🔴 Critical |
| 133 | Flow Publisher Concurrency | FlowPublisherConcurrencyDetector |
Reactive Streams rule 1.3 requires serial signals; a publisher fanning onNext out to a pool corrupts every lock-free subscriber written against it |
🔴 Critical |
| 134 | Record Mutable Component Leak | RecordMutableComponentLeakDetector |
Records are shallowly immutable — a List component shared across threads is a live view, not the snapshot the record appears to be |
🟡 High |
| 135 | Asserting on Findings | (tour, not a bug) | AsyncFindings collects the structured Violation behind every finding, so a test can assert that a detector fired instead of substring-matching its report; also the described awaitUntil and FutureCapture.requireResult |
🟢 Low |
| 136 | Virtual Thread Pooling | VirtualThreadPoolingDetector |
newFixedThreadPool(4, Thread.ofVirtual().factory()) keeps the cap, the queue and the recycled workers — JEP 444's "never pool virtual threads", verbatim |
🟡 High |
| 137 | Platform Thread-Per-Task | PlatformThreadPerTaskDetector |
new Thread(task).start() per webhook costs an OS thread and ~1 MB of stack each — survives the unit test, collapses at the production burst |
🟡 High |
| 138 | Shared SplittableRandom | SharedSplittableRandomDetector |
SplittableRandom shared across workers interleaves a plain read-modify-write — duplicated values and broken statistics with no exception to notice |
🟡 High |
| 139 | CompletableFuture Completion Race | CompletableFutureCompletionRaceDetector |
Several threads complete() one future; the losers get false back and drop their value — or their exception, so an outage reads as a success |
🔴 High |
| 140 | CompletableFuture Cancellation Propagation | CompletableFutureCancellationPropagationDetector |
cancel() completes one future and stops there — the upstream export writes all 50,000 rows anyway, and cancel(true) never interrupts this type |
🔴 High |
| 141 | CompletableFuture Combinator Misuse | CompletableFutureCombinatorMisuseDetector |
allOf/anyOf are constructors, not barriers — the dropped result lets the caller proceed mid-write, and anyOf losers fail with no handler to reach |
🔴 High |
| 142 | Lambda Captured-State Lost Update | LambdaLostUpdateDetector |
hits[0] = hits[0] + 1 from two threads that read the same value — a proven lost update, silent under an atomic or a consistently held monitor |
🔴 High |
| 143 | Virtual Thread Resource Saturation | VirtualThreadResourceSaturationDetector |
The fixed pool was accidental admission control; a thread per task removes it and ten thousand callers queue on a ten-connection pool | 🔴 High |
| 144 | Virtual Thread Monitor Serialization | VirtualThreadMonitorSerializationDetector |
JEP 491 removed the pinning and left the bottleneck — synchronized still admits one thread at a time, and nothing bounds how many arrive |
🟡 Medium |
| 145 | ThreadLocal Cache Degradation | ThreadLocalCacheDegradationDetector |
ThreadLocal<SimpleDateFormat> was a cache because the pool bounded it; per task it allocates one formatter per request and the code never changed |
🟡 Medium |
| 146 | Scope Joiner Misuse | ScopeJoinerMisuseDetector |
onComplete runs on the subtask threads and JEP 525's onTimeout() on the owner — the partial result is copied out of a list another thread is still appending to |
🟠 High |
| 147 | Scope Configuration Misuse | ScopeConfigurationMisuseDetector |
Configuration is immutable, so a lambda that drops what withTimeout returned applies no deadline at all and the scope waits on its slowest subtask forever |
🟠 High |
| 148 | Scope Result Escape | ScopeResultEscapeDetector |
JDK 26 returns a List where JDK 25 returned a Stream; a List stores happily in a field, so the subtask handles are read after the scope that guaranteed them closed |
🔴 High |
| 149 | Lazy Collection Misuse | LazyCollectionMisuseDetector |
List.ofLazy elements compute on whichever thread asks first, so a mapping function reading its own collection deadlocks two threads the JDK's cycle check never sees |
🔴 High |
| 150 | Process Stream Starvation | ProcessStreamStarvationDetector |
Process.waitFor() before anyone reads the child's output returns on a quiet command and hangs forever once a verbose one fills its pipe |
|
| 151 | Blocking On A Non-Blocking Thread | BlockingOnNonBlockingThreadDetector |
a slow lookup on an event-loop thread answers one quote fine and stalls every quote queued behind it under load | 🟡 Medium |
| 152 | Kotlin Mutex Reentrancy | KotlinMutexReentrancyDetector |
a withLock inside the withContext block of the same withLock waits for the outer lock, which waits for the block - kotlinx Mutex is not reentrant, so the refresh never returns and nothing throws |
Examples 114–119 target JDK 24–26 concurrency features. The detectors work off recorded
String-key +Threadevents, so they compile and run on the Java 21 baseline while modeling APIs that only exist on newer JDKs. As of 1.7.0 they are wired into the@AsyncTestpipeline (DetectorType.STABLE_VALUE_MISUSE…SHARED_KDF) and can also be instantiated standalone. They depend on the in-progress build, so install the parent tomavenLocalfirst (mvn -f ../../pom.xml install -DskipTests -Dlicense.mock.mode=true).
The library now includes 4 new important detectors for common concurrency patterns:
What: Detects unclosed HTTP responses, connection pool exhaustion, and incomplete async HTTP operations.
Impact: Resource leaks, connection pool starvation, silent request failures.
Usage:
@AsyncTest(threads = 10, includes = DetectorType.HTTP_CLIENT)
void testHttpClient() {
AsyncTestContext.httpClientDetector()
.recordClientCreated(client, "api-client");
AsyncTestContext.httpClientDetector()
.recordRequestSent(request, "api-call");
AsyncTestContext.httpClientDetector()
.recordResponseReceived(response, "api-call");
}What: Detects InputStream/OutputStream/Reader/Writer instances not properly closed in concurrent code.
Impact: File descriptor leaks, resource exhaustion, locked files.
Usage:
@AsyncTest(threads = 10, includes = DetectorType.STREAM_CLOSING)
void testStreams() throws IOException {
InputStream is = new FileInputStream("data.txt");
AsyncTestContext.streamClosingDetector()
.recordStreamOpened(is, "data-input");
try {
// use stream
} finally {
is.close();
AsyncTestContext.streamClosingDetector()
.recordStreamClosed(is, "data-input");
}
}What: Detects HashMap/LinkedHashMap used as cache without synchronization, concurrent read/write issues.
Impact: Data corruption, ConcurrentModificationException, cache stampede.
Usage:
@AsyncTest(threads = 10, includes = DetectorType.CACHE_CONCURRENCY)
void testCache() {
Map<String, Object> cache = new HashMap<>();
AsyncTestContext.cacheConcurrencyDetector()
.registerCache(cache, "user-cache");
AsyncTestContext.cacheConcurrencyDetector()
.recordPut(cache, "user-cache", "key", value);
AsyncTestContext.cacheConcurrencyDetector()
.recordGet(cache, "user-cache", "key");
}What: Detects missing exception handlers, unjoined futures, and improper CompletableFuture chain usage.
Impact: Swallowed exceptions, resource leaks, incomplete async operations.
Usage:
@AsyncTest(threads = 10, includes = DetectorType.COMPLETABLEFUTURE_CHAIN)
void testCFChain() {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "result");
AsyncTestContext.cfChainDetector()
.recordFutureCreated(future, "async-operation");
CompletableFuture<String> chained = future.thenApply(s -> s.toUpperCase());
AsyncTestContext.cfChainDetector()
.recordChainOperation(future, chained, "thenApply");
AsyncTestContext.cfChainDetector()
.recordExceptionally(future);
String result = chained.join();
AsyncTestContext.cfChainDetector()
.recordFutureJoined(chained, "async-operation");
}Each example is a standalone Maven project that:
- ✅ Passes with
@Test(sequential execution - false confidence) - ❌ Fails with
@AsyncTest(concurrent stress - exposes the real bug) - 📖 Includes detailed comments explaining the problem and solution
NoSuchMethodError: methodParameterTypes when running tests directly in IntelliJ:
This is because IntelliJ's bundled JUnit runner is older than JUnit 6.0.3 used by the examples.
Solution - Run tests via Maven:
- Right-click the test class →
Run 'OrderProcessingServiceTest' via Maven - Or use the Maven tool window → example module →
testlifecycle - Or run from terminal:
mvn clean test
Alternative: Update IntelliJ to the latest version which supports JUnit 6.x
# Run all examples (they pass with @Test)
for dir in examples/*/; do
mvn -f "$dir/pom.xml" clean test
done
# To see the bugs, change @Test to @AsyncTest in any example testexamples/
├── README.md # This file
├── example-01-completablefuture-exception-handling/
│ ├── README.md # Detailed explanation of the bug
│ ├── pom.xml
│ └── src/
│ ├── main/java/.../OrderProcessingService.java # Buggy production code
│ └── test/java/.../OrderProcessingServiceTest.java # Tests + solution
├── example-02-visibility-volatile-flag/
│ ├── pom.xml
│ └── src/
│ ├── main/java/.../TaskProcessorService.java # Buggy production code
│ └── test/java/.../TaskProcessorServiceTest.java # Tests + solution
├── 03-shared-collection/
│ ├── pom.xml
│ └── src/
│ ├── main/java/.../EventAggregatorService.java # Buggy production code (ArrayList + HashMap)
│ └── test/java/.../EventAggregatorServiceTest.java # Tests + solution
└── ... (more examples)
What: Async operations fail without .exceptionally() or .handle(), causing silent data loss.
Impact: Orders/messages disappear without trace. No error logging, no retries, no fallback.
Primary Detector: CompletableFutureExceptionDetector
- Flags: "Unhandled exception in CompletableFuture chain"
- Detects: Exceptions that propagate without being caught
Secondary Detectors:
RaceConditionDetector- Unsynchronized access to shared stateVisibilityMonitor- Inconsistent state visibility across threads
What: Non-volatile shared fields cause threads to cache stale values and never see updates.
Impact: Graceful shutdown hangs, workers run indefinitely, resources leak.
Primary Detector: VisibilityMonitor
- Flags: "Field 'running' accessed by multiple threads without volatile keyword"
- Detects: Non-volatile fields read/written by multiple threads
Secondary Detectors:
BusyWaitDetector- Workers spinning indefinitelyThreadLeakDetector- Workers that never terminate
What: ArrayList and HashMap shared across threads without synchronization.
Impact: Events are silently dropped, counts are wrong, and the application produces corrupted data without throwing any exception.
Primary Detector: SharedCollectionDetector
- Flags: "ArrayList: write operations from N threads — DATA CORRUPTION RISK!"
- Detects: Writes to non-thread-safe collections from multiple threads
Secondary Detectors:
ConcurrentModificationDetector- Reads during concurrent writesRaceConditionDetector- Unsynchronized compound read-modify-write inmerge()
Fix: Use ConcurrentHashMap, CopyOnWriteArrayList, or Collections.synchronizedList()
Five new detectors for JDK types that look thread-safe but corrupt state under unsynchronized concurrent use. All five follow the same manual-recording pattern — test code registers the shared object with the detector before exercising it.
What: Detects java.util.regex.Matcher instances accessed from multiple threads.
Impact: Wrong validation results — valid inputs rejected, invalid inputs accepted — with no exception thrown. Pattern is safe to share; Matcher holds mutable per-match cursor and group state.
Usage:
@AsyncTest(threads = 8, includes = DetectorType.SHARED_MATCHER)
void testEmailValidation() {
AsyncTestContext.sharedMatcherDetector()
.recordAccess(service.getSharedMatcher(), "emailMatcher", Thread.currentThread());
service.validateEmail(email);
}Fix: Call pattern.matcher(input) inside each thread/method call rather than storing the Matcher as a field.
What: Detects DecimalFormat / NumberFormat instances accessed from multiple threads.
Impact: Garbled numeric output (e.g. "1,2345.6" for 1234.56) or ArrayIndexOutOfBoundsException from the formatter's internal digit buffer. The numeric-formatting equivalent of the classic SimpleDateFormat bug.
Usage:
@AsyncTest(threads = 8, includes = DetectorType.SHARED_DECIMAL_FORMAT)
void testAmountFormatting() {
AsyncTestContext.sharedDecimalFormatDetector()
.recordAccess(service.getAmountFormat(), "currencyFmt", Thread.currentThread());
String result = service.formatAmount(amount);
}Fix: ThreadLocal<DecimalFormat> or new DecimalFormat(pattern) per call.
What: Detects two failure modes around WeakReference / SoftReference:
- ERROR — result of
get()used without a null check (referent may be collected between the call and the first dereference) - WARN — referent collected mid-test (some threads saw non-null, others saw null)
Usage:
@AsyncTest(threads = 4, includes = DetectorType.WEAK_REFERENCE_RACE)
void testWeakCache() {
var d = AsyncTestContext.weakReferenceRaceDetector();
Object val = weakRef.get();
d.recordGet(weakRef, "cachedEntry", val, Thread.currentThread());
if (val == null) {
d.recordNullDereference(weakRef, "cachedEntry", Thread.currentThread());
}
// use val — may be null if referent was collected
}What: Detects lambda / Runnable / Callable instances that capture mutable containers (e.g. int[], Object[]) and are executed concurrently while those captures are mutated.
Impact: Silently lost increments, corrupted array contents — the mutation looks like an atomic operation but involves multiple JVM instructions with no synchronization.
Usage:
int[] counter = {0};
Runnable task = () -> { counter[0]++; }; // captures mutable int[]
@AsyncTest(threads = 4, includes = DetectorType.STATEFUL_LAMBDA)
void testCounterTask() {
var d = AsyncTestContext.statefulLambdaDetector();
d.recordExecution(task, "counter-task", Thread.currentThread());
d.recordCapturedMutation(task, "counter", counter, Thread.currentThread());
task.run();
}Fix: Replace int[] with AtomicInteger or LongAdder; or create a new lambda instance per task.
What: Detects MessageDigest instances accessed from multiple threads.
Impact: Wrong hash output with no exception — update() and digest() mutate the internal running buffer and byte count. Hash values differ silently from the expected result. One of the hardest concurrency bugs to reproduce in a debugger.
Usage:
@AsyncTest(threads = 8, includes = DetectorType.SHARED_MESSAGE_DIGEST)
void testFingerprint() {
AsyncTestContext.sharedMessageDigestDetector()
.recordAccess(service.getSha256(), "sha256", Thread.currentThread());
String hash = service.fingerprint(data);
}Fix: MessageDigest.getInstance("SHA-256") per thread, or ThreadLocal<MessageDigest>.
See 10-shared-non-thread-safe-types for a complete
DataProcessingService that shares a Matcher, DecimalFormat, and MessageDigest as
class fields — a pattern common in services written before Java's thread-safety rules were
well understood. The example shows how @Test passes with false confidence and how each
@AsyncTest detector fires.
- Start with
@Test- observe tests pass - Change to
@AsyncTest(threads = 10, invocations = 50, detectAll = true) - Run tests - watch them fail with detailed detector reports
- Read the solution in the test file comments
- Apply the fix - see tests pass again
- Identify similar patterns in your codebase
- Write tests with
@AsyncTest - Let the library's detectors find the exact bugs
- Apply the documented solutions
When contributing new examples:
- Create
example-NN-short-description/directory - Include buggy production code in
src/main/java - Include tests with
@Test(passes) and commented@AsyncTest(fails) - Document the problem, root cause, and solution in comments
- Update this README with the new example
- Copy
pom.xml,build.gradle.ktsandsettings.gradle.ktsfrom an existing example and add the module toexamples/pom.xmlandexamples/settings.gradle.kts. They name no versions: the pom inherits them fromexamples/pom.xml, whose parent isversions/pom.xml, and the Gradle build readslibs.versions.*from the same file.SatelliteVersionsTestfails on a version literal.
To use an example outside this repository, replace each ${name.version} in its pom (or
libs.versions.name.version.get() in its Gradle build) with the value in versions/pom.xml and
drop the <parent> block or the apply(from = ...) line.
The E2E Tests workflow (.github/workflows/e2e-tests.yml) installs the library from source,
then runs the examples as one reactor split across four shards: every example on a push to main
and on the nightly schedule, only the changed ones on a pull request. Each shard runs, in effect:
- name: Install library to local Maven repository
run: mvn -DskipTests clean install
- name: Run this shard's examples
run: mvn -f examples/pom.xml -pl "$modules" test -T 1C -fae "-Dasync-test.strict-detectors=true"What: Detects catch(InterruptedException) blocks that swallow the signal without calling Thread.currentThread().interrupt() or rethrowing.
Impact: Executors, blocking operations, and shutdown handlers can no longer observe the interrupted state. Threads ignore cancellation requests, potentially looping forever.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.INTERRUPT_SWALLOWING)
void testInterruptHandling() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
var d = AsyncTestContext.interruptSwallowingDetector();
d.recordCatch(Thread.currentThread(), "MyWorker.run:42", false); // BAD
// Fix: Thread.currentThread().interrupt(); d.recordCatch(..., true);
}
}Fix: Add Thread.currentThread().interrupt() before returning from every catch block, or rethrow as InterruptedException.
What: Detects SLF4J MDC (Mapped Diagnostic Context) entries not cleared at task end, leaking into the next task on the same pooled thread.
Impact: Log entries for request B carry request A's requestId, userId, or traceId — cross-request log pollution, compliance risks.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.MDC_CONTEXT_LEAK)
void testMdcCleanup() {
var d = AsyncTestContext.mdcContextLeakDetector();
Map<String,String> before = MDC.getCopyOfContextMap();
d.recordTaskStart(Thread.currentThread(), before);
try {
MDC.put("requestId", "abc");
processRequest();
} finally {
d.recordTaskEnd(Thread.currentThread(), MDC.getCopyOfContextMap());
MDC.clear(); // Fix: add this line
}
}Fix: Call MDC.clear() (or MDC.remove(key)) in a finally block.
What: Detects concurrent System.setProperty() / clearProperty() calls during the test run.
Impact: Non-deterministic configuration state, test pollution that survives to subsequent test methods, data races on the shared Properties object.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.SYSTEM_PROPERTY_MUTATION)
void testConfig() {
var d = AsyncTestContext.systemPropertyMutationDetector();
d.recordSet("app.timeout", "5000", Thread.currentThread());
System.setProperty("app.timeout", "5000");
}Fix: Use environment variables, a test-scoped configuration map, or restore the property in @AfterEach.
What: Detects Future / CompletableFuture instances returned from submit() that are never inspected.
Impact: Exceptions thrown by submitted tasks are silently discarded. Failed background work appears to succeed.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.FUTURE_IGNORED)
void testSubmit() {
var d = AsyncTestContext.futureIgnoredDetector();
Future<?> f = executor.submit(task);
d.recordSubmit(f, "orderProcessor", Thread.currentThread());
// Fix: d.recordInspect(f, Thread.currentThread()); f.get();
}Fix: Always call future.get() (in a try-catch) or attach a .whenComplete() / .exceptionally() handler.
What: Detects System.gc() or Runtime.getRuntime().gc() during concurrent execution.
Impact: Triggers an unpredictable stop-the-world pause, inflating latency measurements and introducing artificial timeouts that mask real concurrency bugs.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.EXPLICIT_GC)
void testEviction() {
var d = AsyncTestContext.explicitGcDetector();
d.recordGcInvocation(Thread.currentThread(), "CacheManager.evict:58");
System.gc(); // Flagged!
}Fix: Remove explicit GC calls and rely on the JVM's automatic memory management.
What: Detects calls to Thread.stop(), Thread.suspend(), Thread.resume(), Thread.destroy(), Thread.countStackFrames().
Impact: stop() releases all monitors held by the target thread, breaking all invariants in shared state. suspend/resume are inherently deadlock-prone. All are removed or made no-ops in Java 20+.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.DEPRECATED_THREAD_API)
void testCancel() {
var d = AsyncTestContext.deprecatedThreadApiDetector();
d.recordApiUse("Thread.stop", Thread.currentThread()); // Flagged!
workerThread.stop(); // DO NOT USE
}Fix: Use cooperative cancellation (volatile boolean cancelled, interrupt()), Semaphore, wait/notify, or structured concurrency.
What: Detects DocumentBuilder, SAXParser, Transformer, and XPath instances accessed from multiple threads.
Impact: Corrupted parse results, ConcurrentModificationExceptions, or wrong XPath evaluations that are difficult to reproduce.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.SHARED_XML_PARSER)
void testXmlProcessing() {
var d = AsyncTestContext.sharedXmlParserDetector();
d.recordAccess(sharedBuilder, "DocumentBuilder", Thread.currentThread());
Document doc = sharedBuilder.parse(stream); // Flagged!
}Fix: Use ThreadLocal<DocumentBuilder> or obtain a new instance per task (factories are thread-safe for newXxx()).
What: Detects synchronized blocks locking on cached boxed primitives.
Impact: Any code anywhere in the JVM synchronizing on the same value accidentally shares your monitor, causing surprising contention or deadlocks.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.BOXED_PRIMITIVE_LOCK)
void testSync() {
var d = AsyncTestContext.boxedPrimitiveLockDetector();
Integer id = 42; // cached!
d.recordLockAcquire(id, Thread.currentThread(), "OrderService:30");
synchronized (id) { ... } // Flagged!
}Fix: Use a dedicated private final Object lock = new Object().
What: Detects TimeZone instances mutated from multiple threads.
Impact: Non-deterministic timezone offsets and IDs — silently wrong date/time arithmetic that is notoriously hard to reproduce.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.SHARED_TIMEZONE)
void testTz() {
var d = AsyncTestContext.sharedTimeZoneDetector();
d.recordMutation(sharedTz, "setRawOffset", Thread.currentThread());
sharedTz.setRawOffset(3600_000); // Flagged!
}Fix: Use ZoneId (java.time) which is immutable and thread-safe; or obtain a fresh TimeZone.getTimeZone(id) copy per thread.
What: Detects threads started without a custom UncaughtExceptionHandler that subsequently throw.
Impact: The exception is only printed to stderr via the default thread-group handler. The submitting code has no way to detect the failure, and the thread pool silently replaces the dead thread.
Usage:
@AsyncTest(threads = 4, includes = DetectorType.UNCAUGHT_EXCEPTION_HANDLER)
void testWorker() {
var d = AsyncTestContext.uncaughtExceptionHandlerDetector();
Thread worker = new Thread(task); // no handler set!
d.recordThreadStart(worker);
worker.start();
// if worker throws: d.recordUncaughtException(worker, throwable);
}Fix: Call worker.setUncaughtExceptionHandler(handler) before start(), or use a ThreadFactory that installs a handler on every created thread.
Examples 114–119 target concurrency features introduced or finalized in JDK 24–26.
Each detector is implemented against String keys + Thread (or Object instances), so it
compiles and runs on the Java 21 baseline while modeling APIs that exist only on JDK 24/25/26.
All of them are wired into the @AsyncTest pipeline via DetectorType constants and can
also be instantiated standalone — instantiate, record events, call analyze().
What: StableValue<T> is a deferred-immutable holder, settable at most once and then
constant-folded by the JVM. Detects read-before-set (NoSuchElementException), double-set
(lost update / IllegalStateException), and reentrant orElseSet suppliers.
Detect:
var d = new StableValueMisuseDetector();
d.recordRead("CONFIG", Thread.currentThread()); // before any set → flagged
d.recordSet("CONFIG", Thread.currentThread());
d.recordSet("CONFIG", Thread.currentThread()); // double set → flagged
assertTrue(d.analyze().hasIssues());What: The JDK 25 StructuredTaskScope.open(Joiner) API enforces
open → fork* → join → get* → close. Detects fork-after-join, Subtask.get() before join,
owner-confinement violations (WrongThreadException), and close-without-join.
Detect:
var d = new StructuredTaskScopeMisuseDetector();
Thread owner = Thread.currentThread();
d.recordScopeOpened("s", owner);
d.recordFork("s", "a", owner);
d.recordJoin("s", owner);
d.recordFork("s", "late", owner); // fork after join → flagged
assertTrue(d.analyze().hasIssues());What: A stateful Gatherer on a parallel stream needs a combiner to run in parallel;
without one the JDK evaluates it sequentially and loses nothing. Detects one state object
shared across parallel segments (a data race, HIGH) and, at LOW, a gatherer with no combiner
whose integrator ran on more than one thread (the stage ran sequentially).
Detect:
var d = new GathererConcurrencyMisuseDetector();
d.registerGatherer("running", /*hasCombiner*/ false, /*parallel*/ true);
// integrator: d.recordIntegrate("running", Thread.currentThread());
assertTrue(d.analyze().hasIssues());What: LazyConstant.of(supplier) — the renamed, simplified StableValue — computes at
most once on first get() and rejects null. Detects reentrant suppliers
(IllegalStateException), null-producing suppliers (NPE on JDK 26), hand-rolled holders
whose computation runs more than once, non-deterministic suppliers, and compute convoys.
Detect:
var d = new LazyConstantMisuseDetector();
d.recordComputeStart("CONFIG", Thread.currentThread());
d.recordComputeEnd("CONFIG", Thread.currentThread(), null); // null result → flagged
assertTrue(d.analyze().hasIssues());What: JDK 26 warns on reflective Field.set of final fields and a future release
denies it. The JMM's final-field publication guarantee only covers constructor writes —
reflective writers race every reader, who may see the stale value forever. Detects any
mutation (HIGH), escalating when foreign threads read the field or multiple threads write
it (CRITICAL).
Detect:
var d = new FinalFieldMutationDetector();
d.recordMutation("Config.MAX_RETRIES", Thread.currentThread()); // flagged
assertTrue(d.analyze().hasIssues());What: javax.crypto.KDF is documented not thread-safe; concurrent
deriveKey()/deriveData() on one instance can interleave provider state and silently
derive wrong keys. Detects any KDF instance accessed from more than one thread.
Detect:
var d = new SharedKdfDetector();
d.recordAccess(kdf, "HKDF-SHA256", "deriveKey", threadA);
d.recordAccess(kdf, "HKDF-SHA256", "deriveKey", threadB); // 2 threads → flagged
assertTrue(d.analyze().hasIssues());- @Test gives false confidence: Sequential tests don't expose concurrent bugs
- @AsyncTest finds real problems: Stress testing with barriers exposes race conditions, visibility issues, and unhandled exceptions
- Always handle async exceptions: Use
.exceptionally(),.handle(), or equivalent - Use volatile for shared flags: Any field read/written by multiple threads needs
volatileorAtomic*types - Test under concurrent load: What works sequentially often fails under real concurrent access