Use when a bug only appears under load on shared in-process state (cache, counter, pool, lazy singleton), or a race needs a regression test before the fix. Forces interleavings with barriers and latches, and runs ThreadSanitizer, go test -race with goleak, or jcstress for the JVM.
97%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The canonical home for this skill is testland/race-condition-test-author
Two complementary tools: a deterministic test that forces the bad
interleaving with a start gate (or a hook inside the critical window),
and a detector or stress harness (TSan, go test -race, jcstress) that
explores interleavings you did not think of. A race fix is done when a
test that failed before the fix passes after it, repeatedly.
Check each of: module-level / static mutable variables; singletons with mutable fields; get-then-set on caches or maps without a lock or CAS; per-request data stored on shared objects; pools; lazy initialisation ("if not initialised, initialise"). For each, write down the two operations that must not interleave.
A start gate makes both threads enter together; a hook inside the window makes the collision certain instead of likely:
import threading
def test_lazy_init_creates_one_instance(monkeypatch):
in_window = threading.Barrier(2, timeout=0.5)
real_build = LazyService._build
def slow_build(self):
try:
in_window.wait() # unfixed: both threads meet here, inside the window
except threading.BrokenBarrierError:
pass # fixed: only one thread gets here; carry on
return real_build(self)
monkeypatch.setattr(LazyService, "_build", slow_build)
target = LazyService()
results, errors = [None, None], []
def worker(i):
try:
results[i] = target.lazy_get()
except Exception as e: # surface worker failures to the test thread
errors.append(e)
threads = [threading.Thread(target=worker, args=(i,)) for i in (0, 1)]
for t in threads: t.start()
for t in threads: t.join(timeout=10)
assert not errors
assert results[0] is results[1], "lazy init built two instances"Verified with a check-then-build LazyService: the unfixed code fails
3/3 runs (both threads meet inside the window and build twice); with a
lock around check-and-build it passes 3/3, because only one thread
reaches _build and its barrier wait times out after 0.5 s. The same
test serves before and after the fix.
Assertions belong on the test thread. An assertion or t.Fatal
inside a worker thread, goroutine or executor task can fail silently;
collect results and errors and assert after join().
-raceC / C++ (ThreadSanitizer):
clang -fsanitize=thread -g -O1 program.c -o program
TSAN_OPTIONS="halt_on_error=0" ./program; echo "exit=$?"TSan typically costs 5-15x time and 5-10x memory (ThreadSanitizer).
A report changes the exit status to exitcode (default 66); setting
exitcode=0 in CI makes a run that found races pass
(TSan flags). Never suppress a finding to get green: a
suppression is for third-party code you cannot change, with a ticket.
TSAN_OPTIONS=enable_adaptive_delay=1 injects delays at
synchronisation points to expose more interleavings (ThreadSanitizer).
Go: go test -race ./.... The race detector needs cgo, and typically
costs 5-10x memory and 2-20x time (Go race detector). The full
Go workflow (GORACE, -count / -cpu stress, start gates with
channels, goroutine-leak gating with goleak) is in
references/go.md; goleak filter choices are in
references/goleak-filter-options.md.
The race detector finds unsynchronised memory access only; it says
nothing about goroutines that never exit.
jcstress runs @Actor methods concurrently on a fresh @State
object per trial and classifies results with @Outcome
(jcstress). Put the shared field on the state object so
every trial publishes it anew, and mark the illegal observation
FORBIDDEN so one occurrence fails the run:
import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;
import static org.openjdk.jcstress.annotations.Expect.*;
@JCStressTest
@Outcome(id = "0, 0", expect = ACCEPTABLE, desc = "Nothing seen yet")
@Outcome(id = "1, 1", expect = ACCEPTABLE, desc = "Both writes seen")
@Outcome(id = "0, 1", expect = ACCEPTABLE, desc = "Only x seen")
@Outcome(id = "1, 0", expect = FORBIDDEN, desc = "y seen without x")
@State
public class PublicationTest {
volatile int x, y; // remove volatile to watch 1, 0 become possible
@Actor public void writer() { x = 1; y = 1; }
@Actor public void reader(II_Result r) { r.r1 = y; r.r2 = x; }
}java -jar target/jcstress.jar -t PublicationTestA clean jcstress run means the forbidden outcome was not observed in the interleavings explored, not that it cannot happen; say so when closing a ticket.
Virtual threads (Java 21+) make thousands of concurrent tasks cheap:
@Test
void placesTenThousandOrdersWithoutLoss() throws Exception {
var service = new OrderService();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = IntStream.range(0, 10_000)
.mapToObj(i -> executor.submit(() -> service.place(i)))
.toList();
for (var f : futures) f.get(); // rethrows task failures here
}
assertEquals(10_000, service.totalProcessed());
}Property-based concurrency (Hypothesis) checks invariants over random operation mixes:
import threading
from hypothesis import assume, given, strategies as st
@given(st.lists(st.tuples(st.sampled_from(["read", "write"]), st.integers()), min_size=10, max_size=100))
def test_counter_ends_on_a_written_value(ops):
writes = [v for op, v in ops if op == "write"]
assume(writes)
counter = ThreadSafeCounter()
threads = [threading.Thread(target=(lambda v=v: counter.set(v)) if op == "write" else counter.get)
for op, v in ops]
for t in threads: t.start()
for t in threads: t.join()
assert counter.get() in writesVolume tests raise the odds; they never prove absence. Pair them with Step 2 for the specific bug.
-count=50,
pytest --count=50 with pytest-repeat, or rounds inside the test):
it must pass every time.-cpu 1,2,4 / more rounds, and repeat step 1.- name: Go race detector
env: { CGO_ENABLED: "1" }
run: go test -race -count=3 -cpu 1,4 -timeout 20m ./...
- name: TSan build + test
run: |
cmake -B build-tsan -DCMAKE_C_FLAGS="-fsanitize=thread -g -O1" -DCMAKE_CXX_FLAGS="-fsanitize=thread -g -O1"
cmake --build build-tsan
./build-tsan/test_runner # non-zero (66) when TSan reports
- name: jcstress
run: java -jar target/jcstress.jar -t PublicationTestKeep at least one instrumented matrix leg; never mark it
continue-on-error, and never set exitcode=0.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
sleep() to "force" interleaving | Flaky; passes on fast machines | Start gate or in-window hook (Step 2) |
| Assertions inside worker threads / goroutines | Failures vanish; test passes | Collect, then assert on the test thread |
| One green run after the fix | Races are probabilistic | Repeated runs (Step 6) |
Detector step continue-on-error or exitcode=0 | Findings never fail the build | Blocking step (Step 7) |
| Suppressing a finding in your own code | Ships the race | Fix it, or ticket + owner |
Build tag excluding concurrent tests from -race | The detector never sees the contended code | Run them under -race |
-race find data races, not logical races in correctly
synchronised multi-step operations; Step 2 tests cover those.mvcc-isolation-tests.-fsanitize=thread, runtime
options, adaptive delay@JCStressTest, @Actor, @Outcome outcomesexitcode (default 66), halt_on_error, suppressionsdeadlock-detection-harness -
sister skill for deadlock-specific patternsasync-ordering-tests -
sister skill for async/await orderingmvcc-isolation-tests - database transaction racestestland/qa
aa16752
Canonical home
since Jul 23, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.