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.
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
ConcurrentHashMapguarantees 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
putIfAbsentevaluates 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
CopyOnWriteArrayListprovides 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
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
