Concurrent Collections and Parallel Streams
Overview
Standard collections (ArrayList, HashMap, etc.) are not thread-safe. The java.util.concurrent package provides collections designed for concurrent access without requiring external synchronization.
ConcurrentHashMap
A thread-safe Map that allows concurrent reads and fine-grained locked writes (segment-level, not the whole map).
javaConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.get("a"); // 1
// atomic conditional operations
map.putIfAbsent("b", 2); // adds only if key absent
map.computeIfAbsent("c", k -> k.length()); // compute only if absent
map.merge("a", 1, Integer::sum); // atomic read-modify-writeExam tip:
ConcurrentHashMapdoes not allownullkeys ornullvalues.HashMapallows onenullkey and multiplenullvalues.
CopyOnWriteArrayList
Every write (add, set, remove) creates a fresh copy of the underlying array. Reads are lock-free and always see a consistent snapshot.
javaCopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.add("b");
// safe to iterate while another thread modifies
for (String s : list) {
System.out.println(s); // iterates over the snapshot at iteration start
}| Operation | Cost |
|---|---|
| Read / iterate | Very cheap — no locking |
| Write (add/remove) | Expensive — full array copy |
Best for read-heavy, write-rare scenarios (e.g., listener lists).
BlockingQueue
A queue that blocks on take() when empty and on put() when full. Foundation for the producer-consumer pattern.
| Implementation | Capacity |
|---|---|
LinkedBlockingQueue |
Optionally bounded (default: Integer.MAX_VALUE) |
ArrayBlockingQueue |
Always bounded — capacity specified at creation |
PriorityBlockingQueue |
Unbounded; elements ordered by priority |
javaBlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// producer thread
queue.put("task"); // blocks if full
// consumer thread
String task = queue.take(); // blocks if empty
// non-blocking alternatives
queue.offer("task"); // returns false if full
queue.poll(); // returns null if empty
queue.offer("task", 1, TimeUnit.SECONDS); // timed offer
queue.poll(1, TimeUnit.SECONDS); // timed pollConcurrentLinkedQueue
A non-blocking, unbounded, thread-safe queue based on CAS operations. Does not block — poll() returns null if empty.
javaConcurrentLinkedQueue<String> clq = new ConcurrentLinkedQueue<>();
clq.offer("a");
clq.offer("b");
String head = clq.poll(); // "a" — or null if emptySuitable for high-throughput queues where blocking is undesirable.
Collections.synchronizedList() vs Concurrent Collections
Collections.synchronizedList() wraps an existing list with a single mutex. All operations use the same lock, including iteration — which must be manually synchronized.
javaList<String> syncList = Collections.synchronizedList(new ArrayList<>());
// iteration MUST be manually synchronized
synchronized (syncList) {
for (String s : syncList) {
System.out.println(s);
}
}| Feature | synchronizedList |
CopyOnWriteArrayList |
|---|---|---|
| Thread safety | Yes (coarse lock) | Yes (copy-on-write) |
| Safe iteration without lock | No | Yes |
| Write performance | Better than COW | Expensive (full copy) |
| Read performance | Requires lock | Lock-free |
Summary: Concurrent Collection Choices
| Need | Use |
|---|---|
| Thread-safe key-value store | ConcurrentHashMap |
| Iterable list, few writes | CopyOnWriteArrayList |
| Producer-consumer with blocking | LinkedBlockingQueue / ArrayBlockingQueue |
| High-throughput non-blocking queue | ConcurrentLinkedQueue |
| Quick wrap of existing list | Collections.synchronizedList() |
Parallel Streams and Thread Safety
Parallel streams distribute work across the common ForkJoinPool. The pipeline itself is safe, but lambdas must not write to non-thread-safe shared state.
java// WRONG — ArrayList is not thread-safe; concurrent add causes corruption
List<Integer> result = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(result::add);
// CORRECT — use collect(); standard collectors are thread-safe
List<Integer> result = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());For aggregation, use thread-safe collectors or reduce with an identity:
javaint sum = IntStream.range(0, 1000).parallel().sum(); // safe — no shared state
Map<Boolean, List<Integer>> partitioned = IntStream.range(0, 100)
.parallel()
.boxed()
.collect(Collectors.partitioningBy(n -> n % 2 == 0)); // safeExam tip:
Collectors.toList(),groupingBy(), and other standard collectors are safe for parallel streams. UsingforEachto accumulate into a mutable non-thread-safe collection is a bug.
Atomic Accumulators in Parallel Streams
When a custom accumulation is needed:
javaAtomicInteger counter = new AtomicInteger();
IntStream.range(0, 1000)
.parallel()
.forEach(i -> counter.incrementAndGet()); // atomic — safe
System.out.println(counter.get()); // 1000Key Rules
ConcurrentHashMapdisallowsnullkeys and values;HashMapallows them.Collections.synchronizedList()requires explicit synchronization on the list object during iteration.CopyOnWriteArrayListiterators reflect the snapshot at iterator creation time — they never throwConcurrentModificationException.BlockingQueue.put()andtake()block indefinitely;offer()andpoll()return immediately (or after a timeout).- Parallel stream collectors from
java.util.stream.Collectorsare designed to be thread-safe in parallel pipelines.