Thread-safe collections

Multithreading · lesson 18 of 38 · 4 min read

Choose between ConcurrentHashMap, CopyOnWriteArrayList and a synchronized wrapper, and know their limits.

Open this lesson in the learning hub

Key points

  • ArrayList and HashMap are not thread-safe. Concurrent writes lose data and can corrupt the structure itself.
  • ConcurrentHashMap locks one bin at a time, so threads working on different keys never wait for each other.
  • Use its atomic methods: merge, compute, putIfAbsent. A get followed by a put is still a race.
  • CopyOnWriteArrayList copies the whole array on every write. Ideal for listener lists, terrible for hot writes.
  • Collections.synchronizedList locks each call, but you still have to synchronize on the list while iterating it.
  • Thread-safe elements never make a compound action safe. Two safe calls in a row are still two separate steps.

Example

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Map<String, Integer> counts = new ConcurrentHashMap<>();
        List<String> events = new CopyOnWriteArrayList<>();
        List<Integer> synced = Collections.synchronizedList(new ArrayList<>());

        ExecutorService pool = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 4; i++) {
            int id = i;
            pool.execute(() -> {
                for (int n = 0; n < 1000; n++) {
                    counts.merge("hits", 1, Integer::sum);   // one atomic step, nothing lost
                    synced.add(n);
                }
                events.add("worker-" + id);
            });
        }
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);

        counts.computeIfAbsent("misses", k -> 0);            // atomic check-then-act
        System.out.println("ConcurrentHashMap : " + new TreeMap<>(counts));
        System.out.println("synchronizedList  : " + synced.size() + " items");
        System.out.println("CopyOnWriteList   : " + events.size() + " events");

        long sum = 0;
        synchronized (synced) { for (int v : synced) sum += v; }   // iteration still needs the lock
        System.out.println("iterated safely   : sum " + sum);
    }
}

A thread-safe collection protects each call, not your sequence of calls.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Multithreading course, and every lesson in it is listed on the Multithreading contents page.