Lesson Completion
Back to course

Concurrent Collections: Thread-Safe Data Structures

Advanced
25 minutes4.8Java

1. The Hook (The "Byte-Sized" Intro)

In a Nutshell: Concurrent collections (java.util.concurrent) are thread-safe without external synchronization. ConcurrentHashMap uses lock striping (multiple locks for segments)—allows concurrent reads/writes. CopyOnWriteArrayList clones array on modification—ideal for read-heavy scenarios. BlockingQueue implementations (ArrayBlockingQueue, LinkedBlockingQueue) block on empty/full—perfect for producer-consumer. Never use synchronized wrappers (Collections.synchronizedMap) for high concurrency!

Think of library. Regular HashMap = one checkout desk (bottleneck). ConcurrentHashMap = multiple checkout desks (parallel service). CopyOnWriteArrayList = photocopied catalog (read anytime, update makes new copy)!


2. Conceptual Clarity (The "Simple" Tier)

💡 The Analogy

  • HashMap: Single library (thread-unsafe)
  • Collections.synchronizedMap: Library with one door lock (slow)
  • ConcurrentHashMap: Library with section locks (fast, concurrent)

3. Technical Mastery (The "Deep Dive")

ConcurrentHashMap vs HashMap

java
// ❌ NOT thread-safe Map<String, Integer> map = new HashMap<>(); // ✅ Thread-safe but slow (one lock) Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>()); // ✅ Thread-safe and fast (lock striping) Map<String, Integer> map = new ConcurrentHashMap<>();

4. Interactive & Applied Code

java
import java.util.concurrent.*; import java.util.*; public class ConcurrentCollectionsDemo { public static void main(String[] args) throws Exception { demonstrateConcurrentHashMap(); demonstrateCopyOnWriteArrayList(); demonstrateBlockingQueue(); } static void demonstrateConcurrentHashMap() throws Exception { System.out.println("=== CONCURRENT HASHMAP ==="); ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // Atomic operations map.put("A", 1); map.putIfAbsent("A", 2); // Won't replace System.out.println("Value: " + map.get("A")); // 1 // Atomic compute map.compute("A", (k, v) -> v + 10); // 1 + 10 = 11 System.out.println("After compute: " + map.get("A")); // Concurrent modification safe ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 0; i < 100; i++) { final int val = i; executor.submit(() -> map.put("Key" + val, val)); } executor.shutdown(); executor.awaitTermination(1, TimeUnit.SECONDS); System.out.println("Map size: " + map.size()); } static void demonstrateCopyOnWriteArrayList() { System.out.println("\n=== COPY ON WRITE ARRAY LIST ==="); CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>(); list.add("A"); list.add("B"); list.add("C"); // Iterator never throws ConcurrentModificationException for (String item : list) { System.out.println(item); list.add("D"); // Safe during iteration! // But iterator won't see "D" (snapshot) } System.out.println("Final list: " + list); // Use case: Event listeners (mostly reads, rare writes) List<Runnable> listeners = new CopyOnWriteArrayList<>(); listeners.add(() -> System.out.println("Listener 1")); listeners.add(() -> System.out.println("Listener 2")); // Fire events (concurrent reads) listeners.forEach(Runnable::run); } static void demonstrateBlockingQueue() throws Exception { System.out.println("\n=== BLOCKING QUEUE ==="); BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5); // Producer Thread producer = new Thread(() -> { try { for (int i = 1; i <= 10; i++) { queue.put(i); // Blocks if queue full System.out.println("Produced: " + i); Thread.sleep(100); } } catch (InterruptedException e) {} }); // Consumer Thread consumer = new Thread(() -> { try { Thread.sleep(500); // Start slower for (int i = 1; i <= 10; i++) { Integer item = queue.take(); // Blocks if queue empty System.out.println("Consumed: " + item); Thread.sleep(200); } } catch (InterruptedException e) {} }); producer.start(); consumer.start(); producer.join(); consumer.join(); } } // Comparison example class CollectionPerformanceTest { public static void main(String[] args) throws Exception { int THREADS = 10; int OPERATIONS = 10000; // Test ConcurrentHashMap Map<Integer, Integer> concurrentMap = new ConcurrentHashMap<>(); long start = System.currentTimeMillis(); runTest(concurrentMap, THREADS, OPERATIONS); long concurrentTime = System.currentTimeMillis() - start; // Test synchronized HashMap Map<Integer, Integer> syncMap = Collections.synchronizedMap(new HashMap<>()); start = System.currentTimeMillis(); runTest(syncMap, THREADS, OPERATIONS); long syncTime = System.currentTimeMillis() - start; System.out.println("ConcurrentHashMap: " + concurrentTime + "ms"); System.out.println("Synchronized Map: " + syncTime + "ms"); // ConcurrentHashMap is typically 2-3x faster! } static void runTest(Map<Integer, Integer> map, int threads, int ops) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(threads); for (int i = 0; i < ops; i++) { final int key = i; executor.submit(() -> map.put(key, key)); } executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); } }

5. The Comparison & Decision Layer

CollectionReadsWritesUse When
ConcurrentHashMapFastFastHigh concurrency
CopyOnWriteArrayListVery fastSlowRead-heavy
BlockingQueueBlockingBlockingProducer-consumer
ConcurrentLinkedQueueLock-freeLock-freeHigh throughput

6. The "Interview Corner" (The Edge)

The "Killer" Interview Question: "Why can't you use HashMap.size() reliably with concurrent modifications?" Answer: size() is not atomic! With ConcurrentHashMap, size() is approximate during concurrent modifications:

java
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // Thread 1 adds items // Thread 2 calls size() // size() may return stale value! // Solution: Use mappingCount() for accurate count long count = map.mappingCount(); // More accurate

Pro-Tip: When to use CopyOnWriteArrayList:

java
// ✅ GOOD: Event listeners (rare modifications) List<Listener> listeners = new CopyOnWriteArrayList<>(); listeners.add(listener); // Rare listeners.forEach(l -> l.onEvent()); // Frequent // ❌ BAD: Frequent modifications List<String> cache = new CopyOnWriteArrayList<>(); for (int i = 0; i < 10000; i++) { cache.add("Item" + i); // Copies array 10,000 times! } // Use ConcurrentHashMap or synchronizedList instead

Rule: CopyOnWriteArrayList only if reads >> writes (10:1 ratio minimum)!

Topics Covered

Java FundamentalsConcurrency

Tags

#java#multithreading#threads#concurrency#parallelism

Last Updated

2025-02-01