Thread-safe collections

Read the explanation
What if

Two threads add to one ArrayList. Both write into the same slot, and one element is lost.

0123456size → 0T1: slot[0] = 0[0]=0size = 1size → 0T2: slot[0] = 0[0]=0size = 1

Tap any block to see what that thread is doing.

  • Running
Step 1/5

Two threads add to one ArrayList. add() reads size, writes that slot, then bumps size.

1static final List<Integer> list = new ArrayList<>(); // not thread-safe2 3static void addMany() {4    for (int i = 0; i < 1_000; i++) {5        list.add(i); // read size, write slot [size], then size = size + 16    }7}

Explanation

What just happened

ArrayList.add() is several steps: read size, write the element into slot size, then store size + 1. Both threads read size 0 before either had stored the new size. Both wrote slot 0, and both set size to 1. Two adds, one element. With a resize happening at the same moment, add() can even throw ArrayIndexOutOfBoundsException.

Try the what-if switch

Which to use

Need Use
Shared map ConcurrentHashMap
List read often, changed rarely CopyOnWriteArrayList
Hand work between threads BlockingQueue
Quick fix for old code Collections.synchronizedList / synchronizedMap

Watch out

A thread-safe collection makes each call safe, not your sequence of calls:

counts.put(w, counts.get(w) + 1);  // two calls: still loses updates
counts.merge(w, 1, Integer::sum);  // one atomic call

Use the atomic methods: merge, compute, putIfAbsent, computeIfAbsent.

Common questions

Is ArrayList thread-safe?

No. Two threads calling add() at once can write into the same slot, so an element is lost, or add() throws ArrayIndexOutOfBoundsException during a resize. HashMap has the same problem.

What is the difference between synchronizedList and ConcurrentHashMap?

Collections.synchronizedList puts one lock around every call, so all threads take turns. ConcurrentHashMap locks per key (per bin): updates to different keys don't wait for each other.

Why does my ConcurrentHashMap still lose updates?

map.put(k, map.get(k) + 1) is two calls, and another thread can update in between. Use one atomic call instead: merge(k, 1, Integer::sum), compute or computeIfAbsent.

Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us