Java Concurrency – 15 Real-World Problems Every Senior Developer Should Understand


Your Java code works perfectly with one thread. The tests pass, the output is correct, and performance looks good.

But what happens when 1,000 threads execute the same code simultaneously?

Concurrency introduces problems that are difficult to reproduce in development: race conditions, deadlocks, thread starvation, shared-state corruption, slow asynchronous pipelines, and resource exhaustion.

For senior Java developers, knowing how to create a thread is not enough. You must understand how threads share data, coordinate execution, compete for resources, and behave under production load.

This guide explores 15 real-world Java concurrency problems with detailed answers, practical code examples, and production troubleshooting advice.

1. Two Threads Increment the Same Counter. Why Can the Final Value Be Incorrect?

Scenario: Two threads increment a shared counter 1,000 times each. You expect 2,000, but the final value is smaller.

Why does this happen?

The expression counter++ is not an atomic operation. Conceptually, it involves reading the current value, calculating the next value, and writing the result.

Thread A reads counter = 10
Thread B reads counter = 10

Thread A writes counter = 11
Thread B writes counter = 11

Expected: 12
Actual:   11

This is called a race condition. Both threads operate on the same shared state, and their operations overlap in an unsafe way.

How do you fix it?

For a simple counter that requires atomic increments, use AtomicInteger:

private final AtomicInteger counter =
    new AtomicInteger();

public void increment() {
    counter.incrementAndGet();
}

For more complex operations involving multiple related fields, use a suitable lock or synchronization strategy:

private int counter;

public synchronized void increment() {
    counter++;
}

For high-contention statistical counters, LongAdder may offer better throughput than a single atomic counter, although reading its value is not an atomic snapshot of all concurrent updates.

Production lesson: Use atomic operations for simple shared updates and locks when a group of operations must maintain a shared invariant.

2. Multiple Threads Update a Shared HashMap. What Problems Can Occur?

Scenario: Several threads read and write to the same HashMap. The application experiences inconsistent data or unexpected behavior.

Why does this happen?

HashMap is not designed for unsynchronized concurrent modification. Concurrent reads and writes can lead to lost updates, inconsistent observations, and other incorrect behavior.

Map<String, Integer> counts =
    new HashMap<>();

counts.put("orders", 10);

// Multiple threads update counts concurrently.

Even replacing the map with a thread-safe implementation does not automatically make every compound operation safe.

How should you fix it?

For a shared map with concurrent access, choose a data structure based on the operation.

Option 1: ConcurrentHashMap

private final Map<String, Integer> counts =
    new ConcurrentHashMap<>();

public void increment(String key) {
    counts.merge(key, 1, Integer::sum);
}

merge() provides an atomic update for the specified key. The example avoids a separate read followed by a write.

Option 2: Synchronization

If a business operation must coordinate multiple map entries or other shared state, use a suitable lock or synchronized critical section.

Production lesson: Thread-safe containers protect their supported operations. They do not automatically make an entire business workflow atomic.

3. Two Threads Acquire Locks in Different Orders and Stop Making Progress. What Happened?

Scenario: The application stops responding. Two threads are blocked, and neither can continue.

Why does this happen?

This is a classic deadlock. Each thread holds a resource that the other thread needs.

Thread A:
  Acquires Lock 1
  Waits for Lock 2

Thread B:
  Acquires Lock 2
  Waits for Lock 1

Neither thread can proceed because each waits for the other to release its lock.

How do you prevent deadlocks?

  • Acquire locks in a consistent global order.
  • Avoid holding locks while calling remote services or performing slow I/O.
  • Keep synchronized sections small.
  • Use timed lock acquisition where appropriate.
  • Review code that acquires multiple locks.

For example, if two accounts must be locked during a transfer, always acquire their locks in a stable order based on account ID rather than the direction of the transfer.

For explicit locks, tryLock() can help implement bounded waiting and recovery logic.

How do you investigate a production deadlock?

Capture multiple thread dumps and look for threads in BLOCKED states, lock ownership, and cycles of threads waiting for locks held by one another.

Production lesson: Deadlocks are best prevented through lock-ordering rules and careful critical-section design. Thread dumps help confirm the cause.

4. A Synchronized Method Becomes a Performance Bottleneck. How Would You Improve It?

Scenario: A synchronized service method protects shared data, but response times increase when concurrent traffic grows.

Why does this happen?

Only one thread at a time can execute a synchronized instance method for the same object monitor. Other threads attempting to enter that critical section must wait.

Thread A ----> Enters synchronized method
Thread B ----> Waits
Thread C ----> Waits
Thread D ----> Waits

If the synchronized method performs slow database queries or external API calls, the lock may be held much longer than necessary.

How should you improve it?

  • Move work that does not require shared-state protection outside the critical section.
  • Use atomic classes for simple independent updates.
  • Use concurrent collections when their semantics fit the operation.
  • Consider read-write locks when read-heavy workloads justify their complexity.
  • Reduce the amount of shared mutable state.
  • Measure contention before changing the synchronization mechanism.

For example, do not hold a lock while waiting for a remote service if the protected state can be updated safely in a shorter critical section.

Production lesson: Synchronization provides correctness, but overly broad locking can destroy throughput. Optimize only after identifying the contention.

5. A Volatile Variable Is Used for a Counter. Why Doesn't It Guarantee Atomic Increments?

Scenario: A developer marks a counter as volatile but still observes lost increments.

Why does this happen?

volatile provides visibility and ordering guarantees for accesses to that variable. It does not turn a compound read-modify-write operation into an atomic operation.

private volatile int counter;

public void increment() {
    counter++;
}

Two threads can still read the same value and overwrite one another's increments.

What should you use instead?

Use an atomic type for an independently updated counter:

private final AtomicInteger counter =
    new AtomicInteger();

public void increment() {
    counter.incrementAndGet();
}

Use synchronized or a lock when multiple operations must be performed as one critical section.

A volatile flag remains useful for communicating a simple state change between threads:

private volatile boolean shutdownRequested;

However, a flag alone may not be enough to coordinate thread termination. Use interruption, executor shutdown, or another suitable coordination mechanism when required.

Production lesson: Visibility and atomicity are different properties. Choose a concurrency primitive that guarantees the behavior your code needs.

6. A CompletableFuture Pipeline Fails Midway. How Do You Recover Gracefully?

Scenario: An API combines customer, order, and payment information asynchronously. One task fails, and the final response is incomplete or fails unexpectedly.

Why does this happen?

A CompletableFuture pipeline propagates normal results and exceptional completion according to how its stages are composed. If an earlier stage fails, dependent stages that require its result may not execute normally.

CompletableFuture<Customer> customer =
    loadCustomer();

CompletableFuture<Orders> orders =
    loadOrders();

CompletableFuture<Payment> payment =
    loadPayment();

Each future needs a clear failure policy. Is the entire request invalid if payment details are unavailable, or can the API return customer and order data with a partial result?

How should you handle failures?

Use the appropriate completion-stage methods:

  • exceptionally() to provide a fallback result after failure.
  • handle() to process either a result or an exception.
  • whenComplete() for observing completion without replacing the result by design.
  • thenCompose() to chain another asynchronous operation.
  • thenCombine() to combine results from independent futures.
CompletableFuture<String> future =
    CompletableFuture.supplyAsync(
        this::loadPaymentStatus,
        executor
    ).exceptionally(ex -> {
        log.error("Payment lookup failed", ex);
        return "UNKNOWN";
    });

Use a fallback only when it is semantically valid. Returning UNKNOWN is safer than falsely claiming that a payment succeeded.

For independent operations, allOf() can coordinate completion, but the application must still inspect individual results and decide how failures should affect the response.

Production lesson: Every asynchronous pipeline needs explicit rules for failure, timeout, cancellation, and partial results.

7. A Task Submitted to ExecutorService Never Finishes. How Would You Diagnose It?

Scenario: A task is submitted to an executor, but its future never completes.

Possible causes

  • The task is blocked waiting for I/O.
  • The task is waiting for a lock.
  • The task is waiting for another future that never completes.
  • The executor has no available worker threads.
  • A task is waiting for work queued to the same saturated executor.
  • The task contains an infinite loop or unbounded retry.
  • The task is waiting on a condition that is never signalled.

How should you investigate it?

Start with executor metrics and thread dumps.

Check active thread count, queue depth, completed tasks, rejected tasks, and task duration. Inspect worker stack traces to see whether threads are running, blocked, or waiting.

For a ThreadPoolExecutor, choose bounded queues and an explicit rejection policy based on the business workload.

For tasks waiting on external services, configure timeouts. For interruptible blocking operations, support interruption and cancellation correctly.

Do not simply increase the number of worker threads. If the real problem is a blocked dependency or a lock cycle, more threads may only increase resource consumption.

Production lesson: A task that never finishes is often a symptom of blocked resources, dependency failure, or executor starvation.

8. A Thread Pool Is Exhausted Because Tasks Wait for Other Tasks in the Same Pool

Scenario: All worker threads execute parent tasks. Each parent task submits a child task to the same executor and waits for it. The child tasks never start.

Why does this happen?

This is a form of thread starvation or executor deadlock.

Executor has 4 worker threads

Worker 1: Parent waits for Child 1
Worker 2: Parent waits for Child 2
Worker 3: Parent waits for Child 3
Worker 4: Parent waits for Child 4

All workers are occupied.
Child tasks remain queued.

The executor cannot run the child tasks because every worker is blocked waiting for them.

How do you prevent it?

  • Avoid blocking on child tasks submitted to the same constrained executor.
  • Compose asynchronous stages instead of calling blocking get() or join() unnecessarily.
  • Separate workloads when they have different resource and blocking characteristics.
  • Set explicit timeouts for unavoidable waits.
  • Monitor queue depth and worker utilization.

For example, use thenCompose() to chain a dependent asynchronous operation without occupying a worker solely to wait for another future.

Simply creating a second executor can help isolate workloads, but it does not fix every dependency cycle or resource bottleneck.

Production lesson: Design executors around task behavior and dependencies, not just the number of available CPU cores.

9. ConcurrentHashMap vs synchronizedMap: When Should You Choose Each?

Scenario: Multiple threads access a shared map. You need to select a suitable thread-safe implementation.

Feature ConcurrentHashMap Collections.synchronizedMap()
Concurrency model Supports concurrent retrievals and updates with finer-grained coordination Wraps the map with synchronized access
Concurrent reads Designed to support concurrent access efficiently Access through the wrapper is synchronized
Null keys and values Does not permit null keys or values Depends on the wrapped map's behavior, subject to synchronization
Compound operations Provides atomic methods such as compute and merge Compound workflows need appropriate external synchronization

Example using ConcurrentHashMap:

ConcurrentHashMap<String, Integer> counts =
    new ConcurrentHashMap<>();

counts.merge("orders", 1, Integer::sum);

With synchronizedMap(), iteration generally requires synchronizing on the returned map wrapper:

Map<String, Integer> map =
    Collections.synchronizedMap(new HashMap<>());

synchronized (map) {
    for (var entry : map.entrySet()) {
        System.out.println(entry);
    }
}

Choose based on access patterns and correctness requirements. If several map operations and other shared state must change together, a concurrent map alone may not be enough.

Production lesson: Thread-safe collection access does not guarantee atomicity for an arbitrary sequence of operations.

10. A parallelStream() Operation Performs Worse Than a Sequential Stream

Scenario: A developer changes stream() to parallelStream(), expecting a performance improvement, but the API becomes slower under production load.

Why does this happen?

Parallel streams split work across multiple tasks, which introduces scheduling, coordination, and result-combination overhead.

They may perform worse when:

  • The collection is small.
  • Each operation is very cheap.
  • The workload is unevenly distributed.
  • The pipeline performs blocking I/O.
  • The machine is already CPU-saturated.
  • Multiple concurrent requests compete for shared execution resources.
  • The pipeline has ordering or stateful-operation constraints.

Parallel streams commonly use the shared ForkJoinPool for their execution model. Blocking operations can interfere with other work that relies on the same pool.

How should you decide?

Benchmark representative workloads and measure total latency, throughput, CPU usage, and contention. Compare sequential and parallel implementations under realistic concurrent traffic.

For I/O-heavy workloads, an explicit executor or an asynchronous design with controlled concurrency may be more appropriate.

Production lesson: Parallel execution is not automatically faster. Use it when the workload and measurements justify it.

11. A Task Must Wait Until Another Thread Completes Initialization. Which Mechanism Fits Best?

Scenario: A worker thread must not start processing until another thread finishes loading required configuration.

Which mechanism should you use?

For one-time coordination between threads, CountDownLatch is often a good fit.

CountDownLatch initialized = new CountDownLatch(1);

void initialize() {
    loadConfiguration();
    initialized.countDown();
}

void process() throws InterruptedException {
    initialized.await();
    startProcessing();
}

The worker waits at await() until the latch reaches zero. The latch is one-shot: once opened, it cannot be reset for another initialization cycle.

For other coordination requirements, choose the appropriate tool:

  • CountDownLatch: Wait for one or more events to complete once.
  • Semaphore: Limit concurrent access to a resource.
  • BlockingQueue: Coordinate producers and consumers through data transfer.
  • CompletableFuture: Represent and compose asynchronous results.
  • Condition: Wait for a specific state change associated with a lock.

Always consider failure behavior. If initialization fails and the latch is never released, waiting threads may remain blocked indefinitely. Use timeouts or an explicit failure signal where appropriate.

Production lesson: Select synchronization utilities based on the coordination requirement rather than using sleep() to guess when another thread will finish.

12. Multiple Threads Must Coordinate Before Proceeding to the Next Stage

Scenario: Several worker threads process separate parts of a task. None should move to the next phase until all workers finish the current phase.

Which mechanism fits best?

A CyclicBarrier is designed for a fixed group of threads that must meet at a synchronization point before proceeding.

CyclicBarrier barrier = new CyclicBarrier(3);

Runnable worker = () -> {
    try {
        performPhaseOne();
        barrier.await();

        performPhaseTwo();
    } catch (Exception ex) {
        log.error("Worker failed", ex);
    }
};

Each of the three participating workers waits at the barrier. When all parties arrive, they can proceed to the next phase.

A Phaser can be more appropriate when the number of participants changes or when there are multiple synchronization phases with more flexible registration and deregistration.

Important: If one worker fails before reaching the barrier, the other workers may wait indefinitely unless failure handling, timeouts, or cancellation are designed appropriately.

Production lesson: Coordination utilities can simplify multi-stage concurrency, but the design must account for worker failure and cancellation.

13. A Producer Generates Data Faster Than Consumers Can Process It. How Do You Implement Backpressure?

Scenario: A service receives data faster than downstream workers can process it. Queue size and memory usage continue to increase.

Why does this happen?

If producers can submit unlimited work while consumers process at a fixed rate, the backlog grows until latency, memory pressure, or resource exhaustion becomes a problem.

Producer rate: 10,000 tasks/second
Consumer capacity: 4,000 tasks/second

Backlog grows by approximately:
6,000 tasks/second

A larger queue may delay failure, but it does not fix a sustained mismatch between production and consumption rates.

How should you implement backpressure?

Use bounded buffering and define what happens when capacity is reached.

BlockingQueue<Task> queue =
    new ArrayBlockingQueue<>(1000);

boolean accepted = queue.offer(task);

if (!accepted) {
    // Reject, defer, or apply an explicit overload policy.
}

Possible strategies include:

  • Bounded queues.
  • Rate limiting.
  • Rejecting requests with a clear overload response.
  • Pausing or slowing producers where the protocol permits it.
  • Batching work when appropriate.
  • Scaling consumers when the downstream resource can support it.
  • Using reactive-streams backpressure in systems built around compatible publishers and subscribers.

Do not use an unbounded queue as a substitute for capacity planning.

Production lesson: Backpressure protects the system by preventing incoming work from exceeding what downstream components can safely handle.

14. Virtual Threads Are Enabled, but Database Requests Still Become Slow

Scenario: A Java application uses virtual threads to handle a large number of concurrent requests, but response times increase when database traffic grows.

Why don't virtual threads eliminate every bottleneck?

Virtual threads reduce the cost of maintaining large numbers of concurrent threads, particularly for workloads that spend significant time waiting on supported blocking operations.

They do not increase the capacity of the database, external APIs, CPU, or connection pool.

Many Virtual Threads
        |
        v
Limited Database Connections
        |
        v
Requests Wait for Connections
        |
        v
High Response Time

If an application can run thousands of concurrent tasks but has a limited number of database connections, many tasks may wait for a connection.

Other bottlenecks include:

  • Database locks and slow queries.
  • Downstream service rate limits.
  • CPU-intensive work.
  • Memory usage from large numbers of in-flight requests.
  • Application-level synchronization.
  • Resource pools with limited capacity.

What should you do?

Measure database connection wait time, query latency, active requests, pool utilization, and downstream response times. Apply admission control or concurrency limits where necessary.

Virtual threads are not a reason to remove resource limits. The application must still respect the capacity of the resources it uses.

Production lesson: Virtual threads can improve concurrency scalability, but they cannot make a constrained database process unlimited work.

15. Hundreds of Threads Are Blocked and Requests Are Timing Out. How Do You Find the Root Cause?

Scenario: A production Spring Boot application becomes unresponsive. Request latency increases, and thread dumps show hundreds of waiting or blocked threads.

Step 1: Identify the thread states

Common Java thread states include:

  • RUNNABLE: Executing or otherwise runnable at the JVM level; the thread may still be waiting for operating-system resources.
  • BLOCKED: Waiting to acquire an intrinsic monitor.
  • WAITING: Waiting indefinitely for another thread or synchronization event.
  • TIMED_WAITING: Waiting for a bounded period.

Thread state alone is not enough to identify the cause. Examine stack traces and lock information.

Step 2: Capture multiple thread dumps

For a HotSpot-based JVM, you can use:

jcmd <pid> Thread.print

Capture several dumps a short time apart, where operationally appropriate. Comparing them helps distinguish persistent blocking from normal short-lived waiting.

Step 3: Look for patterns

  • Many threads waiting for the same lock may indicate lock contention or deadlock.
  • Many threads waiting inside database connection acquisition may indicate pool exhaustion.
  • Many threads blocked in HTTP client calls may indicate slow or unavailable downstream services.
  • Many workers waiting for tasks from a saturated executor may indicate thread starvation.
  • Repeated hot stack traces in runnable threads may point toward CPU-intensive code.

Step 4: Correlate thread dumps with metrics

Inspect active request count, executor queue depth, database connection pool usage, downstream latency, CPU usage, GC activity, and p95/p99 response times.

For deeper analysis, use Java Flight Recorder or a profiler to investigate CPU consumption, lock contention, and other runtime events.

Production lesson: Thread dumps show what threads are doing at a moment in time. Combining them with metrics and traces reveals why requests are slow.

A Practical Java Concurrency Troubleshooting Checklist

When a concurrency-related production incident occurs, use a repeatable investigation process.

  1. Confirm the symptom: Determine whether the issue is high latency, incorrect results, deadlock, or resource exhaustion.
  2. Check recent changes: Review deployments, executor settings, synchronization changes, and traffic patterns.
  3. Inspect thread behavior: Capture thread dumps and compare multiple samples.
  4. Inspect shared state: Look for unsynchronized fields, compound updates, and unsafe collection access.
  5. Check executor health: Examine active threads, queue depth, task duration, and rejected tasks.
  6. Check resource limits: Review database connections, HTTP client pools, CPU, and memory.
  7. Measure before tuning: Use metrics, traces, JFR, and profiling to locate the actual bottleneck.
  8. Test under realistic concurrency: Reproduce the workload with controlled load and failure scenarios.

Common Java Concurrency Mistakes

  • Assuming volatile makes compound operations atomic.
  • Using HashMap concurrently without suitable coordination.
  • Holding locks while performing slow I/O.
  • Waiting indefinitely on futures without a timeout or recovery strategy.
  • Submitting dependent tasks to a saturated executor and blocking on them.
  • Assuming parallelStream() always improves performance.
  • Using Thread.sleep() instead of proper synchronization.
  • Creating unbounded queues to absorb unlimited work.
  • Assuming virtual threads remove database and downstream limits.
  • Increasing thread counts before understanding why existing threads are blocked.

Key Takeaways

  • Race conditions arise when shared state is accessed without sufficient coordination.
  • Use atomic classes for simple atomic updates and locks for compound invariants.
  • Concurrent collections are useful, but they do not make arbitrary multi-step workflows atomic.
  • Deadlocks often result from inconsistent lock ordering.
  • Keep critical sections small and avoid blocking I/O while holding locks.
  • Understand the difference between visibility and atomicity.
  • Handle asynchronous exceptions, timeouts, cancellation, and partial results explicitly.
  • Design executors around workload and resource constraints.
  • Choose concurrency utilities based on their coordination semantics.
  • Use bounded queues and backpressure to control overload.
  • Virtual threads improve concurrency ergonomics but do not increase downstream capacity.
  • Use thread dumps, metrics, tracing, and profiling together during production investigations.

Frequently Asked Questions

1. What is the most common Java concurrency problem?

Race conditions and unsafe shared mutable state are common problems. Other important failure modes include deadlocks, thread starvation, executor exhaustion, and unbounded work queues.

2. What is the difference between volatile and AtomicInteger?

volatile provides visibility and ordering guarantees for variable access but does not make compound updates atomic. AtomicInteger provides atomic operations such as incrementing and compare-and-set.

3. When should I use ConcurrentHashMap?

Use it when multiple threads need efficient concurrent access to a shared map and its supported per-key operations fit your requirements. If a workflow must update multiple entries or other state atomically, additional coordination may be required.

4. Why can a thread pool become exhausted?

Workers may be blocked on I/O, locks, or other tasks, or the executor may receive work faster than it can process it. Unbounded waiting and dependencies between tasks in the same pool can also cause starvation.

5. Do virtual threads replace ExecutorService?

No. Virtual threads change how concurrent tasks can be represented and scheduled, but applications still need lifecycle management, resource limits, cancellation, timeouts, and overload control. Executors remain useful for managing task execution.

6. How do you diagnose a deadlock in Java?

Capture thread dumps and inspect the locks each thread owns and waits for. Look for a cycle of dependencies in which each thread waits for a resource held by another thread. Then review lock ordering and critical sections.

Conclusion

Java concurrency is not just about creating threads or using an executor.

It is about controlling shared state, coordinating execution, managing limited resources, and ensuring the application continues to behave correctly under simultaneous requests and partial failures.

A counter can lose updates. A thread-safe map can still be used incorrectly. A future can fail without the caller noticing. A thread pool can become exhausted even when the machine has available CPU. Virtual threads can make concurrency easier while the database remains the bottleneck.

Senior developers understand not only how concurrency works, but also how it fails.

That understanding helps them design systems that are correct under load, easier to troubleshoot, and more reliable in production.

Which of these 15 concurrency problems would you find hardest to debug in a production environment?

Related LogicBrace Guides

  • Senior Java Interview Questions – JVM, Spring Transactions & Design Patterns
  • Java Production Problems – 20 Real-World Scenarios with Deep Answers
  • Spring Boot Mistakes That Cause Production Problems
  • Spring Boot Observability – Top 40 Senior Interview Questions
  • Java Memory Leaks – Top 30 Senior Interview Questions
  • Microservices Failure Scenarios – Top 40 Senior Interview Questions
  • Java API Performance Optimization – Top 40 Senior Interview Questions
Java Concurrency – 15 Real-World Problems Every Senior Developer Should Understand

Post a Comment

0 Comments

Close Menu