Synchronized vs Concurrent Collections
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.
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<>()));
}
}HashMap: 102145
ConcurrentHashMap: 200000Explanation: 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
addorget. - Not safe for compound actions such as "check, then add", unless you lock yourself.
- Iteration must be locked manually, or it can throw
ConcurrentModificationException:
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
| Class | Use instead of | How it stays safe |
|---|---|---|
ConcurrentHashMap | Hashtable, synchronized HashMap | Lock-free reads; writes lock only one bucket |
CopyOnWriteArrayList | Synchronized ArrayList | Every write copies the array; reads never lock |
CopyOnWriteArraySet | Synchronized Set | Built on CopyOnWriteArrayList |
ConcurrentLinkedQueue / ConcurrentLinkedDeque | Synchronized queues | Lock-free linked nodes |
ConcurrentSkipListMap / ConcurrentSkipListSet | Synchronized TreeMap / TreeSet | Sorted, lock-free skip list |
ArrayBlockingQueue, LinkedBlockingQueue | Hand-written wait and notify | Threads 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 collections | Concurrent collections | |
|---|---|---|
| Package | java.util | java.util.concurrent |
| Locking | One lock for the whole collection | Fine-grained locks or no locks |
| Performance with many threads | Poor | Good |
| Iteration | Fail-fast; must lock manually | Fail-safe; never throws |
| Atomic compound operations | No | Yes (merge, putIfAbsent...) |
null | Allowed if the wrapped class allows it | ConcurrentHashMap 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
- Easy: Run
UnsafeCountfive times and note theHashMapresults. Then change it to useCollections.synchronizedMap(new HashMap<>()). Is the result now correct? Why? - Harder: Use a
CopyOnWriteArrayListof listeners and have one thread add listeners while another thread loops over them. Explain why no exception occurs.
Key Takeaways
Was this tutorial helpful?