Why ThreadLocal and ConcurrentHashMap Can Still Cause Bugs—and How to Fix Them

This article examines common misconceptions about Java concurrency utilities such as ThreadLocal, ConcurrentHashMap, and CopyOnWriteArrayList, demonstrates real‑world bugs caused by thread reuse and non‑atomic operations, and provides concrete solutions and performance‑tested alternatives.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Why ThreadLocal and ConcurrentHashMap Can Still Cause Bugs—and How to Fix Them

1 ThreadLocal misuse and user data leakage

In a servlet container (e.g., Tomcat) a ThreadLocal<Integer> is used to cache the current user ID. Because the container reuses worker threads, a thread may retain the ID from a previous request, causing the first read of the ThreadLocal to return stale data.

1.1 Reproduction

Configure Tomcat to use a single worker thread so the same thread handles every request: server.tomcat.max-threads=1 Request from user 1 yields null then 1. Request from user 2 yields 1 then 2, demonstrating the leak.

1.2 Fix

Always clear the ThreadLocal in a finally block so reused threads start with a clean state.

try {
    // business logic that uses threadLocal
} finally {
    threadLocal.remove();
}

1.3 ThreadLocalRandom

ThreadLocalRandom.current()

creates a seed per thread. Storing a single instance in a static field and sharing it across threads defeats the purpose; each thread must obtain its own instance via ThreadLocalRandom.current().

2 ConcurrentHashMap atomicity limits

ConcurrentHashMap

guarantees thread‑safety for individual operations (e.g., get, put) but compound actions such as size() followed by putAll() are not atomic.

2.1 Problem scenario

Ten threads each read map.size(), compute how many entries are missing to reach a target of 1000, log the value, and then insert the missing entries with putAll(). The final map size ends up at 1549 instead of the expected 1000 because the size check and insertion race.

2.2 Solutions

Wrap the whole update in an external lock so only one thread performs the calculation and insertion.

Prefer lock‑free atomic methods. Example using computeIfAbsent together with a thread‑safe counter ( LongAdder) eliminates the need for explicit locking and yields a performance gain of at least 5×.

ConcurrentHashMap<K, LongAdder> map = new ConcurrentHashMap<>();
// inside each worker thread
map.computeIfAbsent(key, k -> new LongAdder()).increment();

2.3 computeIfAbsent vs. putIfAbsent

putIfAbsent

evaluates the value expression even when the key already exists, potentially wasting expensive computation. computeIfAbsent evaluates the mapping function lazily—only when the key is absent. putIfAbsent allows null values only in HashMap; ConcurrentHashMap rejects nulls.

3 High‑performance counting with ConcurrentHashMap

Goal: count occurrences of 10 keys using up to 10 concurrent threads, each performing 10 million random increments.

3.1 Implementation

ConcurrentHashMap<Integer, LongAdder> counter = new ConcurrentHashMap<>();
for (int i = 0; i < 10_000_000; i++) {
    int key = ThreadLocalRandom.current().nextInt(10);
    counter.computeIfAbsent(key, k -> new LongAdder()).increment();
}

3.2 Performance test

A StopWatch measurement shows the lock‑free version (using computeIfAbsent + LongAdder) is at least five times faster than a version that synchronizes on a plain HashMap.

3.3 Underlying CAS operation

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                 Node<K,V> c, Node<K,V> v) {
    return U.compareAndSetObject(tab,
        ((long)i << ASHIFT) + ABASE, c, v);
}

This low‑level compare‑and‑set is used internally by ConcurrentHashMap to achieve lock‑free updates.

4 CopyOnWriteArrayList characteristics

CopyOnWriteArrayList

provides thread‑safety by copying the entire backing array on each write operation. Consequently:

Write‑heavy workloads suffer severe performance degradation (hundreds of times slower than a synchronized ArrayList).

Read‑only or read‑dominant workloads benefit: concurrent reads can be up to 24× faster than a synchronized ArrayList.

Use CopyOnWriteArrayList only when reads vastly outnumber writes.

5 Practical guidelines

5.1 Avoid

Assuming a concurrent collection automatically makes all code thread‑safe without considering compound operations.

Choosing a concurrency utility without understanding its performance trade‑offs.

5.2 Adopt

Read the official JDK documentation, understand the intended usage of each API, and verify behavior with unit tests.

Reproduce concurrency bugs locally and conduct thorough stress testing.

Reference: https://zhuanlan.zhihu.com/p/201333611

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.

Performance TestinglongadderConcurrentHashMapThreadLocalJava concurrencycomputeIfAbsentCopyOnWriteArrayList
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.