Synchronized vs Concurrent Collections

Naveen AnamaneniUpdated · 5 min read

When several threads use the same collection at the same time, ordinary classes like ArrayList and HashMap can lose data or throw exceptions. Java offers two kinds of thread-safe collections. Synchronized collections, such as Collections.synchronizedList(...), Vector and Hashtable, put one lock around the whole collection, so only one thread can use it at a time. Concurrent collections, in java.util.concurrent, such as ConcurrentHashMap and CopyOnWriteArrayList, are designed so that many threads can work at once safely. In this lesson, you will learn the difference and when to use each. Threads themselves are covered later in the course.

The Problem: An Unsafe Map

Let's see an example where two threads count into a plain HashMap and a ConcurrentHashMap. The exact numbers for the plain map change from run to run.

UnsafeCount.java▶ Try it
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
public class UnsafeCount {
    static int run(Map<String, Integer> map) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 100_000; i++) {
                map.merge("hits", 1, Integer::sum);
            }
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        return map.get("hits");
    }
 
    public static void main(String[] args) throws InterruptedException {
        System.out.println("HashMap:           " + run(new HashMap<>()));
        System.out.println("ConcurrentHashMap: "
            + run(new ConcurrentHashMap<>()));
    }
}
Output
HashMap:           102145
ConcurrentHashMap: 200000

Explanation: Two threads each add 100,000 hits, so the right answer is 200,000. The HashMap loses updates because both threads read and write the same entry at the same moment. ConcurrentHashMap.merge is atomic, so no update is lost.

Synchronized Collections

Collections.synchronizedList, synchronizedSet and synchronizedMap wrap a normal collection so that every method call takes the same lock.

  • Safe for single calls such as add or get.
  • Not safe for compound actions such as "check, then add", unless you lock yourself.
  • Iteration must be locked manually, or it can throw ConcurrentModificationException:
Locking a synchronized list
List<String> list = Collections.synchronizedList(new ArrayList<>());
synchronized (list) {
    for (String s : list) {
        System.out.println(s);
    }
}
  • Slow under load: threads queue up for the one lock.

Concurrent Collections

ClassUse instead ofHow it stays safe
ConcurrentHashMapHashtable, synchronized HashMapLock-free reads; writes lock only one bucket
CopyOnWriteArrayListSynchronized ArrayListEvery write copies the array; reads never lock
CopyOnWriteArraySetSynchronized SetBuilt on CopyOnWriteArrayList
ConcurrentLinkedQueue / ConcurrentLinkedDequeSynchronized queuesLock-free linked nodes
ConcurrentSkipListMap / ConcurrentSkipListSetSynchronized TreeMap / TreeSetSorted, lock-free skip list
ArrayBlockingQueue, LinkedBlockingQueueHand-written wait and notifyThreads wait when the queue is empty or full

Concurrent collections also offer atomic compound methods, such as putIfAbsent, computeIfAbsent and merge, and their iterators never throw ConcurrentModificationException.

Differences

Synchronized collectionsConcurrent collections
Packagejava.utiljava.util.concurrent
LockingOne lock for the whole collectionFine-grained locks or no locks
Performance with many threadsPoorGood
IterationFail-fast; must lock manuallyFail-safe; never throws
Atomic compound operationsNoYes (merge, putIfAbsent...)
nullAllowed if the wrapped class allows itConcurrentHashMap rejects null

Common Mistakes

Doing if (!map.containsKey(k)) map.put(k, v); on a shared map. Another thread can slip in between; use putIfAbsent or computeIfAbsent.

Iterating a synchronized wrapper without a synchronized block. It can throw while another thread changes it.

Using CopyOnWriteArrayList for lists that change constantly. Every change copies the whole array.

Interview Corner

What is the difference between synchronized and concurrent collections?

Synchronized collections, such as Vector, Hashtable and the Collections.synchronizedXxx wrappers, guard every method with a single lock on the whole collection, so only one thread can access it at a time; compound actions and iteration need extra manual locking. Concurrent collections, such as ConcurrentHashMap, CopyOnWriteArrayList and ConcurrentLinkedQueue, use finer-grained locking or lock-free algorithms so many threads can work in parallel, provide atomic compound operations like merge and putIfAbsent, and have weakly consistent or snapshot iterators that never throw ConcurrentModificationException.

Practice

  1. Easy: Run UnsafeCount five times and note the HashMap results. Then change it to use Collections.synchronizedMap(new HashMap<>()). Is the result now correct? Why?
  2. Harder: Use a CopyOnWriteArrayList of listeners and have one thread add listeners while another thread loops over them. Explain why no exception occurs.

Quick check: Synchronized vs concurrent

4 questions
1How many threads can use a synchronized collection at once?
2Which is the concurrent replacement for Hashtable?
3Do concurrent collection iterators throw ConcurrentModificationException?
4Why is check-then-put unsafe on a shared map?

Key Takeaways

Was this tutorial helpful?

Lesson 299 of 482