Contention & Performance

Blocking I/O Inside synchronized

Blocking I/O Inside synchronized: practice a Java concurrency bug with symptoms like Throughput collapse, High latency, Many blocked threads. Inspect...

  • Inflated critical sections
  • Contention
  • synchronized
  • Java
  • Beginner

Production symptoms

  • Throughput collapse
  • High latency
  • Many blocked threads

Failure scenario

Code

Java example
class AuditWriter {
    synchronized void record(Event event) {
        String payload = render(event);
        slowNetworkWrite(payload);
        count++;
    }
}

Prod Symptoms

A request path updates small shared state, but it also calls logging, audit, database, filesystem, or HTTP code while holding a monitor.

Key signal: The monitor protects more than shared state; it also wraps a slow wait that does not need mutual exclusion.

  • Latency rises sharply under concurrency
  • Many request threads are BLOCKED on the same monitor
  • Throughput becomes roughly serialized by the slow call
  • CPU may be low because threads are waiting for the monitor or slow dependency
  • The service appears stable but cannot keep up

Run Locally

  • Eight tasks take about eight seconds because the sleep is inside synchronized
  • One thread sleeps while holding the monitor
  • Other pool threads are BLOCKED waiting to enter handle
  • The final count is correct, but throughput is poor

What to look for

  • Many threads blocked on BlockingInsideSynchronizedDemo class monitor
  • One thread inside slowIo while still in the synchronized method
  • Serialized elapsed time close to task count multiplied by sleep time
Run
javac BlockingInsideSynchronizedDemo.java
java BlockingInsideSynchronizedDemo
Inspect during sleep
jps
jcmd <pid> Thread.print
jstack <pid>
BlockingInsideSynchronizedDemo.java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class BlockingInsideSynchronizedDemo {
    private static int processed;

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(8);
        List<Future<?>> futures = new ArrayList<>();
        long start = System.nanoTime();

        for (int i = 0; i < 8; i++) {
            final int id = i;
            futures.add(pool.submit(() -> handle(id)));
        }

        Thread.sleep(500);
        System.out.println("Run jcmd now to catch BLOCKED callers.");

        for (Future<?> future : futures) {
            future.get();
        }

        long elapsedMillis = (System.nanoTime() - start) / 1_000_000;
        System.out.println("processed = " + processed);
        System.out.println("elapsed ms = " + elapsedMillis);
        pool.shutdown();
    }

    private static synchronized void handle(int id) {
        slowIo(id);
        processed++;
    }

    private static void slowIo(int id) {
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Note: Thread.sleep stands in for blocking I/O. The synchronized method serializes all callers.

Diagnosis and fix

Explanation

The synchronized method protects the shared counter, but it also holds the monitor during slow work.

Key signal: Hold locks only while touching the state that actually requires protection.

  • Only one caller can enter the synchronized method at a time
  • The thread inside the method waits on the slow dependency while still owning the monitor
  • All other callers block even though the slow operation does not need the shared state
  • Correctness is preserved, but concurrency is destroyed
  • This is an inflated critical section

How to Diagnose

Thread dumps are very useful because monitor contention is visible.

  • Capture a dump while latency is high
  • Look for many request threads in BLOCKED state on the same monitor
  • Find the owner thread and inspect what it is doing while holding the monitor
  • Correlate lock contention with slow dependency metrics, such as audit sink or database latency
  • Measure elapsed time under increasing thread counts
  • Review synchronized methods that call network, database, filesystem, logging, or sleep-like operations
Commands
jps
jstack <pid>
jcmd <pid> Thread.print
Expected dump shape
"pool-1-thread-2" #... BLOCKED (on object monitor)
  at BlockingInsideSynchronizedDemo.handle(BlockingInsideSynchronizedDemo.java:...)
  - waiting to lock <...> (a java.lang.Class for BlockingInsideSynchronizedDemo)

"pool-1-thread-1" #... TIMED_WAITING (sleeping)
  at java.lang.Thread.sleep(Native Method)
  at BlockingInsideSynchronizedDemo.slowIo(BlockingInsideSynchronizedDemo.java:...)

How to Fix

  • Move slow or blocking work outside the synchronized region when it does not need protected state
  • Keep only the shared-state mutation inside the lock
  • Take an immutable snapshot under the lock if the slow call needs protected data
  • Do not split a lock scope if the check, slow operation, and mutation are one correctness invariant
  • Use a queue or dedicated writer when the slow side effect must be serialized separately
  • Measure after the change because correctness bugs can hide in lock-scope refactors
BlockingOutsideSynchronizedFixed.java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class BlockingOutsideSynchronizedFixed {
    private static final Object lock = new Object();
    private static int processed;

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(8);
        List<Future<?>> futures = new ArrayList<>();
        long start = System.nanoTime();

        for (int i = 0; i < 8; i++) {
            final int id = i;
            futures.add(pool.submit(() -> handle(id)));
        }

        for (Future<?> future : futures) {
            future.get();
        }

        long elapsedMillis = (System.nanoTime() - start) / 1_000_000;
        System.out.println("processed = " + processed);
        System.out.println("elapsed ms = " + elapsedMillis);
        pool.shutdown();
    }

    private static void handle(int id) {
        slowIo(id);

        synchronized (lock) {
            processed++;
        }
    }

    private static void slowIo(int id) {
        try {
            Thread.sleep(1_000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Note: The slow operation now runs concurrently; only the counter update is serialized.