Java Concurrency Deep Dive – Part 8: Real‑World Pitfalls and Post‑mortem

This article reviews common Java concurrency pitfalls—including deadlocks, ThreadLocal memory leaks, Integer‑cache loops, unbounded thread‑pool queues, and swallowed exceptions—illustrates each with code samples, analyzes real production incidents, and provides a concise checklist and tool‑selection guide for safe concurrent programming.

Coder Trainee
Coder Trainee
Coder Trainee
Java Concurrency Deep Dive – Part 8: Real‑World Pitfalls and Post‑mortem

1. Deadlock Cases

Transfer deadlock

public class Account {
    private int balance;
    // ❌ Wrong: A transfers to B and B transfers to A simultaneously, causing deadlock
    public void transfer(Account target, int amount) {
        synchronized (this) {
            synchronized (target) {
                this.balance -= amount;
                target.balance += amount;
            }
        }
    }
    // ✅ Correct: acquire locks in a fixed order
    public void transferSafe(Account target, int amount) {
        Account first = this.hashCode() < target.hashCode() ? this : target;
        Account second = this.hashCode() < target.hashCode() ? target : this;
        synchronized (first) {
            synchronized (second) {
                this.balance -= amount;
                target.balance += amount;
            }
        }
    }
}

ThreadLocal memory leak

public class UserContext {
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
    public void process() {
        currentUser.set(new User("张三"));
        // business logic ...
        // forgot to clean up!
    }
}

public void processSafe() {
    try {
        currentUser.set(new User("张三"));
        // business logic ...
    } finally {
        currentUser.remove();
    }
}

Integer cache dead loop

// ❌ Wrong: Integer.valueOf caches -128~127, leading to identical lock objects
public class Counter {
    private static final Map<Integer, Object> locks = new ConcurrentHashMap<>();
    public void increment(Integer id) {
        synchronized (locks.computeIfAbsent(id, k -> new Object())) {
            // business logic
        }
    }
}

// ✅ Correct: use a dedicated Lock object
private static final Map<Integer, ReentrantLock> locks = new ConcurrentHashMap<>();
public void incrementSafe(Integer id) {
    ReentrantLock lock = locks.computeIfAbsent(id, k -> new ReentrantLock());
    lock.lock();
    try {
        // business logic
    } finally {
        lock.unlock();
    }
}

2. ThreadPool Pitfalls

Unbounded queue causing OOM

// ❌ Wrong: LinkedBlockingQueue is unbounded, tasks may pile up and OOM
ExecutorService pool = Executors.newFixedThreadPool(10);

// ✅ Correct: use a bounded queue
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    10, 20, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000), // bounded queue
    new ThreadPoolExecutor.CallerRunsPolicy()
);

Swallowed exceptions

// ❌ Wrong: exception thrown inside task is not caught by outer try‑catch
pool.execute(() -> {
    throw new RuntimeException("error");
});

// ✅ Correct: handle exception inside the task
pool.execute(() -> {
    try {
        // business logic
    } catch (Exception e) {
        log.error("Task execution exception", e);
    }
});

// ✅ Also possible: capture exception via Future
Future<?> future = pool.submit(() -> {
    throw new RuntimeException("error");
});
try {
    future.get();
} catch (ExecutionException e) {
    log.error("Task execution exception", e.getCause());
}

3. Concurrency Tool Selection

Simple counter : AtomicLong – lightweight, high performance.

High‑performance counter : LongAdder – segmented counting, faster than AtomicLong.

Concurrent map : ConcurrentHashMap – high concurrency, lock‑free reads.

Concurrent list : CopyOnWriteArrayList – read‑many, write‑few.

Blocking queue : ArrayBlockingQueue – bounded, FIFO.

Delay queue : DelayQueue – delayed execution.

Read‑write lock : ReentrantReadWriteLock – read‑many, write‑few.

ConcurrentHashMap usage notes

// ❌ Wrong: composite operation is not atomic
if (!map.containsKey(key)) {
    map.put(key, value);
}

// ✅ Correct: use putIfAbsent
map.putIfAbsent(key, value);

// ✅ Correct: use computeIfAbsent
map.computeIfAbsent(key, k -> expensiveCompute(k));

4. Production Incident Reviews

ThreadPool misconfiguration

Symptom: peak‑time response time jumps from 50 ms to 5 s, many requests timeout.

Root cause: corePoolSize set to 10, maxPoolSize to 20, and the queue is an unbounded LinkedBlockingQueue. During peak load tasks accumulate in the queue, causing long wait times.

Fix: adjust pool size based on CPU cores and replace the unbounded queue with a bounded ArrayBlockingQueue.

int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    cpuCores * 2,               // IO‑bound, core threads = CPU*2
    cpuCores * 4,               // max threads = CPU*4
    60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(2000), // bounded queue
    new ThreadPoolExecutor.CallerRunsPolicy()
);

Concurrent modification of a collection

Symptom: occasional ConcurrentModificationException.

Root cause: another thread modifies the collection while it is being iterated.

Fixes:

Use iterator's remove() method.

Replace the collection with a concurrent one such as CopyOnWriteArrayList.

Collect items to delete in a temporary list and remove them in batch.

// ❌ Wrong
List<String> list = new ArrayList<>();
for (String s : list) {
    if (s.equals("test")) {
        list.remove(s); // may throw ConcurrentModificationException
    }
}

// ✅ Correct method 1: iterator.remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("test")) {
        it.remove();
    }
}

// ✅ Correct method 2: use concurrent collection
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

// ✅ Correct method 3: batch removal
List<String> toRemove = new ArrayList<>();
for (String s : list) {
    if (s.equals("test")) {
        toRemove.add(s);
    }
}
list.removeAll(toRemove);

5. Concurrency Best‑Practice Checklist

Use thread pools – avoid creating threads manually.

Use bounded queues – prevent OOM.

Handle exceptions properly – catch and log inside the task.

Clean up ThreadLocal – call remove() in a finally block.

Avoid composite operations – use atomic methods or protect with locks.

Choose appropriate concurrent tools – avoid synchronized for high‑performance counters.

Lock in a fixed order – prevent deadlocks.

Leverage java.util.concurrent – reuse existing utilities.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaConcurrencydeadlockThreadPoolbest-practicesconcurrenthashmapthreadlocal
Coder Trainee
Written by

Coder Trainee

Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.