Non-Functional Testing
Concurrency Testing

What is Concurrency Testing? Race Conditions, Deadlocks & Thread Safety

Parul Dhingra - Senior Quality Analyst
Parul Dhingra13+ Years ExperienceHire Me

Senior Quality Analyst

Updated: 7/8/2025

What is Concurrency Testing? Race Conditions, Deadlocks and Thread SafetyWhat is Concurrency Testing? Race Conditions, Deadlocks and Thread Safety

QuestionQuick Answer
What is concurrency testing?Testing that validates how software behaves when multiple threads, processes, or users access shared resources simultaneously
Why is it hard?Bugs appear intermittently and depend on timing, making them difficult to reproduce
What bugs does it find?Race conditions, deadlocks, livelocks, data corruption, and thread starvation
When should you do it?When your code uses threads, async operations, shared state, or handles multiple users
Key tools?ThreadSanitizer, Helgrind, Java PathFinder, stress testing frameworks

Concurrency testing validates that software behaves correctly when multiple execution paths run simultaneously. It targets bugs that only appear when threads, processes, or users compete for shared resources at specific timing intervals.

These bugs are notoriously difficult to find because they depend on execution timing. The same code can pass thousands of tests, then fail in production when thread scheduling happens differently.

Why Concurrency Bugs Are Different

Concurrency bugs behave differently from typical software defects. A null pointer exception happens every time you hit the buggy code path. A race condition might occur once in 10,000 executions, or only on certain hardware, or only under production load.

This creates three fundamental challenges:

Non-determinism: The same test can pass 99 times and fail once. Thread scheduling decisions made by the operating system vary between runs, between machines, and between load conditions.

Observation affects behavior: Adding logging or attaching a debugger changes timing enough to make bugs disappear. This is called a "Heisenbug" - the act of observing changes what you observe.

Late manifestation: Corruption might occur early but symptoms appear much later. A race condition corrupts a data structure, but the crash happens minutes later when unrelated code reads that structure.

Consider a simple counter increment: counter = counter + 1. This looks like one operation but compiles to three: read the value, add one, write the result. If two threads execute this simultaneously, both might read the same value, add one, and write back - losing an increment.

This type of bug won't crash your application. It won't throw an exception. It will silently produce wrong results that are extremely difficult to trace back to their source.

Types of Concurrency Bugs

Concurrency bugs fall into distinct categories, each requiring different detection strategies.

Data Races

A data race occurs when two threads access the same memory location, at least one access is a write, and there's no synchronization between them. The result depends on which thread "wins" the race.

Thread 1: balance = balance + 100    // deposit
Thread 2: balance = balance - 50     // withdrawal

Without synchronization, the final balance could be any of several values depending on interleaving.

Atomicity Violations

Operations that must complete as a unit get interrupted. Bank transfers are the classic example: debit from one account must pair with credit to another. If the process fails between these steps, money disappears.

Order Violations

Code assumes operations happen in a specific sequence, but concurrency violates that assumption. Initialization might not complete before another thread tries to use the initialized value.

Deadlocks

Two or more threads wait forever for resources held by each other. Thread A holds lock 1 and waits for lock 2. Thread B holds lock 2 and waits for lock 1. Neither can proceed.

Livelocks

Threads keep running but make no progress. Unlike deadlocks where threads freeze, livelocked threads are active but accomplishing nothing - like two people trying to pass each other in a hallway, each stepping the same direction repeatedly.

Starvation

Some threads never get access to shared resources because other threads continuously grab them first. The starving thread runs but can never complete its work.

Race Conditions Explained

Race conditions are the most common concurrency bug. The outcome of the program depends on the relative timing of events - which thread "races" to a resource first.

Check-Then-Act Races

A common pattern: check a condition, then act on it. The problem is that the condition can change between the check and the action.

// Dangerous pattern
if (file.exists()) {
    file.delete();  // What if another thread deleted it first?
}

Between checking existence and deleting, another thread might delete the file. This is a "time-of-check to time-of-use" (TOCTOU) vulnerability.

Read-Modify-Write Races

Any operation that reads a value, modifies it, and writes it back is vulnerable unless synchronized:

// Each operation is three CPU instructions
inventory = inventory - 1;

Two threads might both read inventory = 10, both compute 9, both write 9. You sold two items but decremented inventory by one.

Initialization Races

Object construction isn't atomic. A thread might see a partially constructed object:

// Thread 1
sharedObject = new ComplexObject();

// Thread 2 might see sharedObject as non-null
// but internal fields still uninitialized

This is why the "double-checked locking" pattern is broken in many languages without specific memory barriers.

Detection Strategies

Static analysis tools examine code for patterns known to cause races. They can flag unsynchronized access to shared variables but produce false positives when synchronization exists through indirect means.

Dynamic analysis instruments running code to track memory accesses and detect actual races. ThreadSanitizer and Helgrind take this approach.

Stress testing increases the probability of hitting race windows by running many threads at high speed. More executions means more chances to hit the problematic timing.

Deadlocks and Livelocks

Deadlocks occur when threads form a circular wait for resources. Detection is more straightforward than race conditions because the symptoms are clear: threads stop making progress.

Classic Deadlock Scenario

Thread 1:
    acquire(lock_A)
    acquire(lock_B)  // waits forever if Thread 2 holds lock_B

Thread 2:
    acquire(lock_B)
    acquire(lock_A)  // waits forever if Thread 1 holds lock_A

If both threads execute their first acquire before either executes their second, deadlock results.

Deadlock Conditions

Four conditions must hold for deadlock:

  1. Mutual exclusion: Resources cannot be shared
  2. Hold and wait: Threads hold resources while waiting for others
  3. No preemption: Resources cannot be forcibly taken
  4. Circular wait: A cycle exists in the wait graph

Breaking any condition prevents deadlock. Most practical solutions either order resource acquisition (preventing circular wait) or use timeouts with retry (breaking hold and wait).

Testing for Deadlocks

Lock ordering verification: Ensure all code acquires locks in consistent order. Static analysis can detect ordering violations.

Timeout-based detection: If an operation doesn't complete within expected time, investigate for deadlock. Production systems often implement watchdog timers.

Thread dump analysis: When deadlock is suspected, capture thread states. Threads blocked on locks with circular dependencies confirm deadlock.

Stress testing with varied timing: Run tests that exercise lock acquisition under load. More executions with varied timing increases the chance of hitting deadlock-prone interleavings.

Livelocks

Livelocks are harder to detect because threads remain active. CPU usage stays high, but useful work doesn't complete.

Common cause: retry logic that causes competing threads to repeatedly collide. Each backs off, retries, collides again.

Detection requires monitoring progress metrics, not just thread activity. If requests/second drops while CPU stays high, investigate livelock.

Thread Safety Testing

A component is thread-safe if it behaves correctly when accessed from multiple threads simultaneously. Testing thread safety means verifying this property holds.

What Thread Safety Means

Thread safety isn't binary - it exists on a spectrum:

Immutable: No state changes after construction. Inherently thread-safe.

Thread-compatible: Safe if callers synchronize access externally. The component doesn't corrupt, but callers must coordinate.

Thread-safe: Multiple threads can call any methods without external synchronization.

Thread-hostile: Cannot be safely used from multiple threads even with external synchronization (rare but exists).

Testing Immutability

Verify that no method modifies object state after construction. This can be done through:

  • Code review for field assignments
  • Static analysis for field modifications
  • Runtime monitoring for unexpected mutations

Testing Thread-Safe Components

Concurrent access tests: Multiple threads call methods simultaneously while assertions verify invariants remain intact.

Stress tests: High thread counts with rapid operations maximize the chance of hitting synchronization bugs.

Invariant checking: After concurrent operations, verify data structure invariants still hold. A thread-safe map should never lose entries during concurrent puts.

Example: Testing a Thread-Safe Counter

@Test
void counterShouldBeThreadSafe() throws InterruptedException {
    Counter counter = new Counter();
    int threadCount = 100;
    int incrementsPerThread = 1000;
 
    ExecutorService executor = Executors.newFixedThreadPool(threadCount);
    CountDownLatch latch = new CountDownLatch(threadCount);
 
    for (int i = 0; i < threadCount; i++) {
        executor.submit(() -> {
            for (int j = 0; j < incrementsPerThread; j++) {
                counter.increment();
            }
            latch.countDown();
        });
    }
 
    latch.await();
    assertEquals(threadCount * incrementsPerThread, counter.getValue());
}

This test creates 100 threads, each incrementing 1000 times. A non-thread-safe counter will almost always produce a value less than 100,000 due to lost updates.

Important: A passing test doesn't prove thread safety. Race conditions are probabilistic. The test increases confidence but cannot provide certainty.

Testing Approaches

Different testing approaches target different concurrency bugs with varying trade-offs between coverage and practicality.

Stress Testing

The simplest approach: run many threads executing the code under test at high speed for extended periods.

Strengths: Easy to implement. No special tools required. Can find bugs that static analysis misses.

Weaknesses: Non-deterministic. A test might pass 1000 times then fail. May not cover rare interleavings.

Best practices:

  • Run tests many times (hundreds or thousands)
  • Vary thread counts and timing
  • Monitor for symptoms beyond crashes (data corruption, invariant violations)
  • Run on multiple machine configurations

Systematic Testing

Tools like Java PathFinder or Microsoft CHESS explore different thread interleavings systematically rather than randomly.

Strengths: Can find bugs that random testing misses. Provides better coverage guarantees.

Weaknesses: Computationally expensive. Limited to smaller code sections. May not scale to full applications.

Static Analysis

Tools analyze source code without execution to identify potential concurrency issues.

Strengths: Fast feedback during development. Can check code paths that are hard to reach through testing.

Weaknesses: False positives are common. May miss bugs that depend on runtime values. Cannot detect all categories of concurrency bugs.

Dynamic Analysis

Instruments running code to detect concurrency issues as they occur.

Tools: ThreadSanitizer (C/C++), Helgrind (any language using pthreads), Java Flight Recorder.

Strengths: Finds real bugs in actual execution. Low false positive rate.

Weaknesses: Only finds bugs in executed code paths. Significant performance overhead (often 2-20x slower).

Formal Verification

Mathematical proofs that concurrent code satisfies specifications.

Strengths: Can provide certainty rather than probability. Catches bugs no amount of testing would find.

Weaknesses: Requires specialized expertise. Doesn't scale to large codebases. Specifications themselves might be wrong.

Concurrency Testing Tools

ThreadSanitizer (TSan)

A dynamic race detector built into Clang and GCC compilers. Detects data races at runtime with moderate overhead.

How it works: Instruments memory accesses and synchronization operations. Tracks happens-before relationships. Reports when two accesses could race.

Usage (C/C++):

clang++ -fsanitize=thread -g source.cpp -o program
./program

Limitations: 5-15x runtime overhead. Requires recompilation. May miss races not exercised during execution.

Helgrind

Part of the Valgrind suite. Detects race conditions, deadlocks, and misuse of POSIX threading APIs.

How it works: Runs program in a virtual machine, tracking all memory accesses and pthread operations.

Strengths: No recompilation needed. Comprehensive checking.

Weaknesses: 10-30x overhead. Linux-focused.

Java Concurrency Tools

Java Flight Recorder: Low-overhead production profiling that captures thread states, lock contention, and synchronization issues.

FindBugs/SpotBugs: Static analysis with concurrency-specific checks for Java code.

Thread dump analysis: JVM can dump all thread states on command. Tools like fastThread.io analyze dumps for deadlocks.

Go Race Detector

Built into the Go toolchain. Run tests with -race flag:

go test -race ./...

How it works: Similar to ThreadSanitizer - tracks memory access and goroutine synchronization.

Strengths: Integrated into standard tooling. Low friction to use.

Load Testing Tools

JMeter, Gatling, k6, and Locust can generate concurrent user load that exercises application-level concurrency.

These don't detect bugs directly but create conditions where concurrency bugs manifest as visible failures, timeouts, or data corruption.

Designing Concurrency Tests

Effective concurrency tests require deliberate design to maximize bug detection probability.

Identify Shared State

Map out what data structures are accessed by multiple threads:

  • Instance fields accessed from different threads
  • Static/global variables
  • Database records
  • File system resources
  • External service state

Each shared state element needs test coverage.

Design for Contention

Tests should force threads to compete for resources simultaneously. Use synchronization barriers to align thread execution:

CyclicBarrier barrier = new CyclicBarrier(threadCount);
 
// Each thread:
barrier.await();  // All threads release at the same time
// Execute concurrent operation

Test State Transitions

Concurrency bugs often occur during state changes. Test operations that modify shared state, not just reads:

  • Account balance updates
  • User status changes
  • Inventory modifications
  • Session state transitions

Include Invariant Checks

After concurrent operations complete, verify that data structure invariants hold:

  • No duplicate entries in unique collections
  • Balance sheets sum to zero
  • Reference counts are accurate
  • Linked structures are intact

Repeat Many Times

A single test run provides minimal confidence. Run concurrency tests hundreds or thousands of times:

for i in {1..1000}; do
    ./run_concurrency_tests.sh || echo "Failed on iteration $i"
done

Track failure rates over time. A test that fails once in 1000 runs has a concurrency bug.

Common Patterns and Anti-Patterns

Safe Patterns

Immutable objects: If data never changes after construction, no synchronization needed. Pass immutable values between threads.

Thread confinement: Keep mutable data owned by a single thread. Other threads interact through message passing.

Copy-on-write: Return copies rather than references to shared structures. Modification creates a new copy rather than mutating.

Synchronized collections: Use thread-safe collections (ConcurrentHashMap, CopyOnWriteArrayList) rather than synchronizing manually.

Dangerous Anti-Patterns

Double-checked locking (without proper memory barriers): A flawed optimization that leads to partially constructed objects being visible.

Synchronizing on mutable fields: If the lock object itself can change, different threads might synchronize on different objects.

Lock ordering violations: Acquiring locks in different orders across the codebase invites deadlock.

Holding locks during I/O: Long-held locks increase contention and deadlock risk. Complete I/O outside critical sections.

Mixing synchronization mechanisms: Using both intrinsic locks and ReentrantLock on the same resource creates confusion and bugs.

Documentation Practices

Thread safety properties should be documented explicitly:

/**
 * This class is thread-safe. All methods may be called
 * concurrently from multiple threads.
 *
 * Thread safety is achieved through internal synchronization
 * on the lock object. Callers should not synchronize externally.
 */
public class SafeCache { ... }

Without documentation, developers make wrong assumptions about what's safe to call concurrently.

Integration with Development Workflow

CI Pipeline Integration

Concurrency tests need special handling in continuous integration:

Run tests multiple times: A single run isn't meaningful. Run suites 10-100 times to catch intermittent failures.

Set appropriate timeouts: Deadlocks cause tests to hang forever. Configure timeouts that fail fast.

Track flakiness metrics: A test that fails 1% of the time indicates a real bug, not a flaky test.

Use dedicated agents: Concurrency testing benefits from consistent hardware. Virtualized environments can mask or introduce timing issues.

Pull Request Checks

Include concurrency analysis in code review:

  • Static analysis tools flag potential issues
  • Reviewers check for proper synchronization
  • Tests must pass multiple runs before merge

Production Monitoring

Some concurrency bugs only appear under production load. Monitor for:

  • Thread count growth over time (leak indicator)
  • Lock contention metrics
  • Deadlock detection alerts
  • Request latency distribution changes (P99 outliers suggest contention)

Post-Incident Analysis

When concurrency bugs reach production, conduct thorough analysis:

  • Capture thread dumps at time of failure
  • Review recent changes to affected code
  • Add regression tests that reproduce the timing
  • Document the bug pattern for future prevention

When Concurrency Testing Matters Most

Invest more heavily in concurrency testing when:

Multiple writers to shared state: Read-only concurrent access is safe. Concurrent modifications require synchronization.

Financial or safety-critical operations: Incorrect calculations due to races can have serious consequences.

High-throughput systems: More operations means more opportunities for races. Systems handling thousands of requests per second need rigorous testing.

Distributed systems: Network delays create wider timing windows for races. Distributed consensus is notoriously difficult.

Long-running processes: Bugs that occur once per million operations will happen daily in a system processing millions of operations per day.

Lower priority when:

Single-threaded execution: If the runtime is single-threaded (traditional Node.js, synchronous PHP), most concurrency concerns don't apply.

Stateless request handlers: If each request gets fresh state and doesn't modify shared data, concurrency risk is minimal.

Immutable data: Systems built on immutable data structures avoid most concurrency bugs by design.


Concurrency testing requires accepting uncertainty. Unlike deterministic tests where pass/fail is clear, concurrency tests provide probabilistic confidence. The goal isn't proving correctness - it's finding bugs before users do.

Start with stress testing to catch obvious issues. Add static analysis to catch common patterns. Use dynamic analysis tools when bugs prove elusive. For critical systems, consider systematic testing or formal methods.

The most important step is acknowledging that concurrent code needs different testing than sequential code. Standard unit tests that pass reliably don't validate thread safety. Explicit concurrency testing, with deliberate contention and repeated execution, is essential for systems where multiple threads share mutable state.

Quiz on concurrency testing

Your Score: 0/9

Question: What makes concurrency bugs fundamentally different from other software bugs?

Continue Reading

Frequently Asked Questions (FAQs) / People Also Ask (PAA)

What is concurrency testing and why is it different from regular testing?

What is a race condition and how do I test for it?

What is a deadlock and how can I detect one?

What tools should I use for concurrency testing?

How do I make my code thread-safe?

Why do my concurrency tests pass locally but fail in CI or production?

When should I invest heavily in concurrency testing versus skip it?

How do I integrate concurrency testing into my CI/CD pipeline?