Topic 13.8
Concurrent Collections and Atomics
In one line
java.util.concurrent gives you thread-safe building blocks so you rarely need your own locks: atomic variables (AtomicInteger, LongAdder), concurrent collections (ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue) and synchronizers (CountDownLatch, Semaphore, CyclicBarrier).
Think of it like this
A busy railway station. The ticket counter with a token display hands out the next number to whoever presses the button, one at a time, without a queue manager (an atomic counter). The notice board is copied, updated and swapped in one go so readers never see half a change (copy-on-write). A conveyor belt between the kitchen and the canteen holds a limited number of plates (a blocking queue). The gate opens only when all 5 coaches have been checked (a latch), and only 2 platform ramps can be used at once (a semaphore).
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Atomic variable
- A variable whose updates happen in one indivisible step, safe without locks.
- Compare-and-swap (CAS)
- A CPU instruction that sets a value only if it still equals what you expected, and tells you whether it worked.
- Contention
- Many threads trying to use the same lock or variable at the same moment.
- Weakly consistent iterator
- An iterator that never throws on concurrent changes and may or may not show changes made during iteration.
- Copy-on-write
- Making a fresh copy for every change, so readers keep using the old, unchanged version.
- Latch
- A one-time gate that opens when a count reaches zero.
- Semaphore
- A set of permits that limits how many threads can do something at once.
Step by step
01How compare-and-swap works
incrementAndGet is a loop: read the current value v, try compareAndSet(v, v + 1); if another thread changed it in between, the CAS fails and the loop retries with the new value. No thread ever blocks, and no update is lost.
On x86 this is the lock cmpxchg instruction. Under heavy contention many threads retry, which is why LongAdder splits a hot counter into several cells.
02ConcurrentHashMap: use the atomic methods
if (!map.containsKey(k)) map.put(k, v); is a race even on a ConcurrentHashMap: both threads can pass the check. Use map.putIfAbsent(k, v) or map.computeIfAbsent(k, key -> create()).
Counting with map.merge(word, 1, Integer::sum) is one atomic step per word, so several threads can count into the same map. Keep functions passed to compute* short and don't touch other keys of the same map inside them.
03Iterating while others change the collection
A plain ArrayList throws ConcurrentModificationException when changed during iteration (Topic 9.9). CopyOnWriteArrayList iterates over the array that existed when the loop started; changes go into a new array. ConcurrentHashMap iterators are weakly consistent.
04Latches, semaphores and barriers
Start N workers and wait for all to finish: CountDownLatch(N), each worker calls countDown() in finally, the coordinator calls await(). Limit concurrent calls to a fragile service to 5: Semaphore(5), acquire() before, release() in finally.
Run a simulation in rounds where every thread must finish step k before anyone starts step k + 1: CyclicBarrier(N).
05Which tool for which job
A shared counter: AtomicLong, or LongAdder if very hot. A shared map: ConcurrentHashMap. A read-mostly list: CopyOnWriteArrayList. Hand work from one thread to another: a BlockingQueue. Wait for N events: CountDownLatch. Limit concurrency: Semaphore. Combine atomics with care: two separate atomic variables updated one after the other are not atomic together.
Try it yourself
- 1
Break the word count
In the ConcurrentHashMap example, replace
counts.merge(...)withcounts.put(w, counts.getOrDefault(w, 0) + 1);. On a multi-core JDK some counts come out too low: the get and the put are separate steps. - 2
Change the permits
In the Semaphore example, use
new Semaphore(1)and printmaxInside.get(). It's 1: the semaphore turned the six jobs into one-at-a-time.
Code & diagrams
Expected output
AtomicInteger: 40000
LongAdder: 40000
CAS succeeds only if unchanged: true, now 0The map is copied into a TreeMap only to print it in a fixed order.
Expected output
{be=2, do=3, is=1, not=2, or=2, to=3}Expected output
ArrayList: ConcurrentModificationException
CopyOnWriteArrayList: [a, b, a!, b!]Expected output
all 6 jobs done; never more than 2 at once: trueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Put null into a ConcurrentHashMap
Call counts.put("x", null);.
Myth vs fact
Myth
Using a ConcurrentHashMap makes all my code thread-safe.
Fact
Each method is atomic, but sequences of calls (get then put) aren't. Use merge, compute and putIfAbsent.
Myth
Atomics are always faster than locks.
Fact
Under very heavy contention CAS loops retry a lot. LongAdder or less sharing may be better.
Myth
CopyOnWriteArrayList is a good general thread-safe list.
Fact
Every write copies the whole array. It suits read-mostly lists only.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Since Java 8,
ConcurrentHashMaplocks individual bins withsynchronizedon the first node and uses CAS to insert into empty bins; resizing is cooperative, with threads helping transfer bins. Itssize()uses aLongAdder-like counter, so it's an estimate during concurrent updates. - ▸
LongAdderkeeps abaseplus aCell[]array padded with@Contendedto avoid false sharing between CPU cache lines;sum()adds them up, so it's not an atomic snapshot. - ▸
The ABA problem: a CAS can succeed when a value went A to B and back to A in between.
AtomicStampedReferenceadds a version stamp for algorithms where that matters.
Remember this
- 1
Atomics (
AtomicInteger,AtomicLong,AtomicBoolean,AtomicReference) do read-modify-write in one indivisible step using the CPU's compare-and-swap (CAS) instruction, with no lock.incrementAndGet(),compareAndSet(expected, new),updateAndGet(fn)andaccumulateAndGet(x, fn)cover most needs. Under very heavy contention, **LongAdder** (Java 8) is faster for counters: it spreads updates over several cells and sums them when you read. - 2
**
ConcurrentHashMapis the thread-safe map. Reads don't lock, and writes lock only one bin (bucket), so many threads update in parallel. Use its atomic compound methods**:merge,compute,computeIfAbsent,putIfAbsent. Two separate calls (getthenput) are not atomic together. It rejectsnullkeys and values, and its iterators are weakly consistent: they never throwConcurrentModificationException. - 3
**
CopyOnWriteArrayList** copies its whole array on every write, so iterators see a fixed snapshot and never throw. It's ideal for lists that are read far more than written, such as listener lists, and terrible for frequent writes. - 4
**
BlockingQueue** (ArrayBlockingQueue,LinkedBlockingQueue,PriorityBlockingQueue,SynchronousQueue) connects producer and consumer threads:putwaits when full,takewaits when empty (Topic 13.5). Everything a thread did beforeputting an item happens-before what the taker does after receiving it. - 5
Synchronizers coordinate threads. **
CountDownLatch(n)**: threadsawait()untilcountDown()has been called n times (one-shot). **Semaphore(n)**: at most n threads hold a permit at once (acquire/release), for rate limiting and resource pools. **CyclicBarrier(n): n threads wait for each other, then all continue, and it resets for the next round.Phaser** is a flexible, reusable version. - 6
Wrapping a normal collection with
Collections.synchronizedList/Mapmakes each call synchronized, but iteration and compound actions still need manual locking, and every call contends on one lock. The concurrent collections are designed for multi-threaded use from the start, so prefer them.
Explain it without notes
How does an AtomicInteger increment without a lock?
Why can get-then-put on a ConcurrentHashMap still lose updates, and what should you use?
When would you use CountDownLatch, Semaphore and CyclicBarrier?
Practice
Use a CountDownLatch so main waits for three worker threads, each adding 100 to a shared AtomicInteger.
Trade-offs
- ↔
Lock-free atomics avoid blocking but only cover single variables; multi-variable invariants still need a lock.
- ↔
Concurrent collections scale well for common operations but offer only weakly consistent views; a consistent snapshot of several entries needs extra coordination.
Done when you can
Done when you can explain compare-and-swap and use AtomicInteger, AtomicReference and LongAdder.
Done when you use ConcurrentHashMap's merge/compute/putIfAbsent instead of get-then-put.
Done when you can pick between CopyOnWriteArrayList, BlockingQueue and synchronized wrappers.
Done when you can coordinate threads with CountDownLatch, Semaphore and CyclicBarrier.