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
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
javac BlockingInsideSynchronizedDemo.java
java BlockingInsideSynchronizedDemo
jps
jcmd <pid> Thread.print
jstack <pid>
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
jps
jstack <pid>
jcmd <pid> Thread.print
"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
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.