Concurrent Collections in Java
Learn thread-safe collection classes designed for high-performance concurrent access
Think of concurrent collections like a smart buffet line! Regular collections are like a single-file buffet where only one person can take food at a time - everyone has to wait. Concurrent collections are like a smart buffet with multiple serving stations where many people can grab different foods at the same time without bumping into each other. Java's concurrent collections use clever tricks to let multiple threads access them safely and quickly!
The Problem with Regular Collections
Regular collections like ArrayList and HashMap are NOT thread-safe! If multiple threads try to modify them at the same time, you get corrupted data or crashes. Even using synchronized wrappers is slow because they lock the entire collection. Let's see the problem:
// Problem with regular collections in multithreadingimport java.util.*;public class UnsafeCollectionExample { public static void main(String[] args) { // Regular ArrayList - NOT thread-safe! List<Integer> list = new ArrayList<>(); // Create 10 threads that all add numbers to the list Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++) { final int threadId = i; threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { // Danger! Multiple threads modifying list simultaneously list.add(threadId * 1000 + j); } }); threads[i].start(); } // Wait for all threads for (Thread thread : threads) { try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } // Expected: 10,000 elements (10 threads × 1000) // Actual: Unpredictable! Could be less, or even crash! System.out.println("Expected size: 10000"); System.out.println("Actual size: " + list.size()); System.out.println("Data corruption occurred: " + (list.size() != 10000)); }}/* Output (varies each run):Expected size: 10000Actual size: 9847Data corruption occurred: trueOR you might see:Exception in thread "Thread-3" java.lang.ArrayIndexOutOfBoundsExceptionPROBLEM: ArrayList is NOT thread-safe!Multiple threads cause:- Lost updates (data corruption)- ArrayIndexOutOfBoundsException- Unpredictable results*/CopyOnWriteArrayList - Thread-Safe List
CopyOnWriteArrayList is perfect when you have many readers and few writers! Every time you modify it, it creates a new copy of the underlying array. Sounds expensive? It is! But for read-heavy workloads where writes are rare, it's blazingly fast because reads don't need any locks!
// Using CopyOnWriteArrayList for thread safetyimport java.util.concurrent.CopyOnWriteArrayList;import java.util.List;public class CopyOnWriteExample { public static void main(String[] args) { // Thread-safe list - perfect for many readers, few writers List<String> safeList = new CopyOnWriteArrayList<>(); // Add initial data safeList.add("Apple"); safeList.add("Banana"); safeList.add("Cherry"); System.out.println("=== Testing CopyOnWriteArrayList ===\n"); // Reader threads (many readers) Runnable readTask = () -> { String thread = Thread.currentThread().getName(); for (int i = 0; i < 3; i++) { System.out.println(thread + " reading: " + safeList); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } }; // Writer thread (few writers) Runnable writeTask = () -> { String thread = Thread.currentThread().getName(); try { Thread.sleep(100); safeList.add("Date"); System.out.println(thread + " added 'Date'"); Thread.sleep(100); safeList.add("Elderberry"); System.out.println(thread + " added 'Elderberry'"); } catch (InterruptedException e) { e.printStackTrace(); } }; // Start multiple readers Thread reader1 = new Thread(readTask, "Reader-1"); Thread reader2 = new Thread(readTask, "Reader-2"); Thread reader3 = new Thread(readTask, "Reader-3"); // Start one writer Thread writer = new Thread(writeTask, "Writer"); reader1.start(); reader2.start(); reader3.start(); writer.start(); try { reader1.join(); reader2.join(); reader3.join(); writer.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\nFinal list: " + safeList); System.out.println("\nAll operations completed safely!"); }}/* Output:=== Testing CopyOnWriteArrayList ===Reader-1 reading: [Apple, Banana, Cherry]Reader-2 reading: [Apple, Banana, Cherry]Reader-3 reading: [Apple, Banana, Cherry]Writer added 'Date'Reader-1 reading: [Apple, Banana, Cherry, Date]Reader-2 reading: [Apple, Banana, Cherry, Date]Reader-3 reading: [Apple, Banana, Cherry, Date]Writer added 'Elderberry'Reader-1 reading: [Apple, Banana, Cherry, Date, Elderberry]Reader-2 reading: [Apple, Banana, Cherry, Date, Elderberry]Reader-3 reading: [Apple, Banana, Cherry, Date, Elderberry]Final list: [Apple, Banana, Cherry, Date, Elderberry]All operations completed safely!BENEFITS:- No locks needed for reads (very fast!)- Readers see consistent snapshots- Perfect for read-heavy workloads*/ConcurrentHashMap - High-Performance Thread-Safe Map
ConcurrentHashMap is the superstar of concurrent collections! Unlike Hashtable which locks the entire map, ConcurrentHashMap only locks small segments. It's like having multiple cashiers at a store - many threads can work on different parts of the map simultaneously. Way faster than synchronized HashMap!
// Using ConcurrentHashMap for thread-safe map operationsimport java.util.concurrent.ConcurrentHashMap;import java.util.Map;public class ConcurrentHashMapExample { public static void main(String[] args) { // Thread-safe map with high concurrency Map<String, Integer> wordCount = new ConcurrentHashMap<>(); System.out.println("=== Word Counter with ConcurrentHashMap ===\n"); // Multiple threads counting words String[] thread1Words = {"apple", "banana", "apple", "cherry"}; String[] thread2Words = {"banana", "date", "apple", "banana"}; String[] thread3Words = {"cherry", "apple", "elderberry", "date"}; Runnable counter1 = () -> countWords(wordCount, thread1Words, "Counter-1"); Runnable counter2 = () -> countWords(wordCount, thread2Words, "Counter-2"); Runnable counter3 = () -> countWords(wordCount, thread3Words, "Counter-3"); Thread t1 = new Thread(counter1); Thread t2 = new Thread(counter2); Thread t3 = new Thread(counter3); t1.start(); t2.start(); t3.start(); try { t1.join(); t2.join(); t3.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("\n=== Final Word Counts ==="); wordCount.forEach((word, count) -> { System.out.println(word + ": " + count); }); System.out.println("\nAll counts are correct! No data corruption."); } static void countWords(Map<String, Integer> map, String[] words, String threadName) { for (String word : words) { // Atomic operation - increment or initialize to 1 map.merge(word, 1, Integer::sum); System.out.println(threadName + " counted: " + word); try { Thread.sleep(10); // Simulate processing time } catch (InterruptedException e) { e.printStackTrace(); } } }}/* Output:=== Word Counter with ConcurrentHashMap ===Counter-1 counted: appleCounter-2 counted: bananaCounter-3 counted: cherryCounter-1 counted: bananaCounter-2 counted: dateCounter-3 counted: appleCounter-1 counted: appleCounter-2 counted: appleCounter-3 counted: elderberryCounter-1 counted: cherryCounter-2 counted: bananaCounter-3 counted: date=== Final Word Counts ===apple: 4banana: 3cherry: 2date: 2elderberry: 1All counts are correct! No data corruption.BENEFITS:- High concurrency (multiple threads work simultaneously)- Atomic operations (merge, compute, etc.)- No need for external synchronization- Much faster than synchronized HashMap*/BlockingQueue - Producer-Consumer Pattern
BlockingQueue is magic for producer-consumer scenarios! Producers put items in the queue, consumers take them out. If the queue is full, producers wait. If empty, consumers wait. It's like a conveyor belt in a factory - perfect for coordinating work between threads!
// Using BlockingQueue for producer-consumer patternimport java.util.concurrent.*;class Task { private int id; private String description; public Task(int id, String description) { this.id = id; this.description = description; } public int getId() { return id; } public String getDescription() { return description; } @Override public String toString() { return "Task-" + id + ": " + description; }}public class BlockingQueueExample { public static void main(String[] args) throws InterruptedException { // Bounded queue - max 5 tasks BlockingQueue<Task> taskQueue = new ArrayBlockingQueue<>(5); System.out.println("=== Producer-Consumer with BlockingQueue ===\n"); // Producer: Creates tasks and adds to queue Thread producer = new Thread(() -> { try { for (int i = 1; i <= 10; i++) { Task task = new Task(i, "Process data " + i); System.out.println("Producer creating: " + task); // put() blocks if queue is full! taskQueue.put(task); System.out.println("Producer added: " + task + " (Queue size: " + taskQueue.size() + ")"); Thread.sleep(200); // Simulate task creation time } System.out.println("\nProducer finished creating tasks!\n"); } catch (InterruptedException e) { e.printStackTrace(); } }, "Producer"); // Consumer 1: Takes tasks and processes them Thread consumer1 = new Thread(() -> { try { for (int i = 0; i < 5; i++) { // take() blocks if queue is empty! Task task = taskQueue.take(); System.out.println(" Consumer-1 took: " + task); Thread.sleep(500); // Simulate processing time System.out.println(" Consumer-1 completed: " + task); } } catch (InterruptedException e) { e.printStackTrace(); } }, "Consumer-1"); // Consumer 2: Another worker processing tasks Thread consumer2 = new Thread(() -> { try { for (int i = 0; i < 5; i++) { Task task = taskQueue.take(); System.out.println(" Consumer-2 took: " + task); Thread.sleep(600); // Slower consumer System.out.println(" Consumer-2 completed: " + task); } } catch (InterruptedException e) { e.printStackTrace(); } }, "Consumer-2"); // Start all threads producer.start(); Thread.sleep(100); // Let producer start first consumer1.start(); consumer2.start(); // Wait for completion producer.join(); consumer1.join(); consumer2.join(); System.out.println("\nAll tasks produced and consumed!"); System.out.println("Final queue size: " + taskQueue.size()); }}/* Output:=== Producer-Consumer with BlockingQueue ===Producer creating: Task-1: Process data 1Producer added: Task-1: Process data 1 (Queue size: 1) Consumer-1 took: Task-1: Process data 1Producer creating: Task-2: Process data 2Producer added: Task-2: Process data 2 (Queue size: 1) Consumer-2 took: Task-2: Process data 2Producer creating: Task-3: Process data 3Producer added: Task-3: Process data 3 (Queue size: 1) Consumer-1 completed: Task-1: Process data 1 Consumer-1 took: Task-3: Process data 3Producer creating: Task-4: Process data 4Producer added: Task-4: Process data 4 (Queue size: 1)...Producer finished creating tasks!All tasks produced and consumed!Final queue size: 0BENEFITS:- Automatic thread coordination (no manual wait/notify!)- Producer blocks when queue is full- Consumer blocks when queue is empty- Perfect for work distribution*/ConcurrentLinkedQueue - Lock-Free Queue
ConcurrentLinkedQueue is a lock-free, non-blocking queue! It uses clever atomic operations instead of locks, making it super fast. It's unbounded (grows as needed) and perfect for high-throughput scenarios where you need maximum speed and don't want threads blocking!
// Using ConcurrentLinkedQueue for lock-free operationsimport java.util.concurrent.ConcurrentLinkedQueue;import java.util.Queue;import java.util.concurrent.atomic.AtomicInteger;class Event { private static AtomicInteger idGenerator = new AtomicInteger(0); private int id; private String type; private long timestamp; public Event(String type) { this.id = idGenerator.incrementAndGet(); this.type = type; this.timestamp = System.currentTimeMillis(); } @Override public String toString() { return "Event#" + id + " [" + type + "]"; }}public class ConcurrentLinkedQueueExample { public static void main(String[] args) throws InterruptedException { // Lock-free, thread-safe queue Queue<Event> eventQueue = new ConcurrentLinkedQueue<>(); AtomicInteger producedCount = new AtomicInteger(0); AtomicInteger consumedCount = new AtomicInteger(0); System.out.println("=== Event Processing with ConcurrentLinkedQueue ===\n"); // Multiple event producers Runnable producer = () -> { String threadName = Thread.currentThread().getName(); String[] eventTypes = {"Click", "Scroll", "KeyPress", "MouseMove"}; for (int i = 0; i < 5; i++) { String type = eventTypes[i % eventTypes.length]; Event event = new Event(type); // offer() - non-blocking add eventQueue.offer(event); producedCount.incrementAndGet(); System.out.println(threadName + " produced: " + event + " (Queue size: ~" + eventQueue.size() + ")"); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } }; // Multiple event consumers Runnable consumer = () -> { String threadName = Thread.currentThread().getName(); for (int i = 0; i < 5; i++) { Event event = null; // poll() - non-blocking remove while (event == null) { event = eventQueue.poll(); if (event == null) { // Queue empty, wait a bit try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } consumedCount.incrementAndGet(); System.out.println(" " + threadName + " consumed: " + event); try { Thread.sleep(80); // Simulate processing } catch (InterruptedException e) { e.printStackTrace(); } } }; // Start 3 producers and 3 consumers Thread[] producers = new Thread[3]; Thread[] consumers = new Thread[3]; for (int i = 0; i < 3; i++) { producers[i] = new Thread(producer, "Producer-" + (i + 1)); consumers[i] = new Thread(consumer, "Consumer-" + (i + 1)); producers[i].start(); consumers[i].start(); } // Wait for all threads for (int i = 0; i < 3; i++) { producers[i].join(); consumers[i].join(); } System.out.println("\n=== Final Statistics ==="); System.out.println("Total events produced: " + producedCount.get()); System.out.println("Total events consumed: " + consumedCount.get()); System.out.println("Remaining in queue: " + eventQueue.size()); System.out.println("\nLock-free operation completed successfully!"); }}/* Output:=== Event Processing with ConcurrentLinkedQueue ===Producer-1 produced: Event#1 [Click] (Queue size: ~1)Producer-2 produced: Event#2 [Click] (Queue size: ~2)Producer-3 produced: Event#3 [Click] (Queue size: ~3) Consumer-1 consumed: Event#1 Consumer-2 consumed: Event#2 Consumer-3 consumed: Event#3Producer-1 produced: Event#4 [Scroll] (Queue size: ~1)Producer-2 produced: Event#5 [Scroll] (Queue size: ~2)...=== Final Statistics ===Total events produced: 15Total events consumed: 15Remaining in queue: 0Lock-free operation completed successfully!BENEFITS:- No locks! Uses atomic operations- Non-blocking (never waits for locks)- High throughput- Unbounded capacity*/Real-World Example: Order Processing System
Let's build a realistic order processing system using concurrent collections! This simulates an e-commerce platform where multiple users place orders, payment processors handle payments, and shipping coordinators prepare packages - all happening concurrently with safe data sharing!
// Real-world order processing system with concurrent collectionsimport java.util.concurrent.*;import java.util.concurrent.atomic.AtomicInteger;import java.util.*;class Order { private static AtomicInteger orderIdGenerator = new AtomicInteger(1000); private int orderId; private String customer; private double amount; private String status; public Order(String customer, double amount) { this.orderId = orderIdGenerator.incrementAndGet(); this.customer = customer; this.amount = amount; this.status = "PENDING"; } public int getOrderId() { return orderId; } public String getCustomer() { return customer; } public double getAmount() { return amount; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } @Override public String toString() { return "Order#" + orderId + " [$" + String.format("%.2f", amount) + " - " + customer + " - " + status + "]"; }}public class OrderProcessingSystem { // Thread-safe collections for different stages private static BlockingQueue<Order> pendingOrders = new LinkedBlockingQueue<>(); private static ConcurrentHashMap<Integer, Order> processedOrders = new ConcurrentHashMap<>(); private static CopyOnWriteArrayList<String> processingLog = new CopyOnWriteArrayList<>(); public static void main(String[] args) throws InterruptedException { System.out.println("=== E-Commerce Order Processing System ===\n"); // Customer threads - place orders Runnable customerTask = () -> { String customerName = Thread.currentThread().getName(); Random random = new Random(); for (int i = 0; i < 3; i++) { double amount = 10.0 + random.nextDouble() * 90.0; Order order = new Order(customerName, amount); try { pendingOrders.put(order); String logEntry = customerName + " placed " + order; processingLog.add(logEntry); System.out.println(logEntry); Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } }; // Payment processor threads Runnable paymentProcessor = () -> { String processorName = Thread.currentThread().getName(); try { for (int i = 0; i < 5; i++) { Order order = pendingOrders.poll(2, TimeUnit.SECONDS); if (order == null) break; String logEntry = " " + processorName + " processing payment for " + order; processingLog.add(logEntry); System.out.println(logEntry); Thread.sleep(300); // Simulate payment processing order.setStatus("PAID"); processedOrders.put(order.getOrderId(), order); logEntry = " " + processorName + " completed payment: " + order.getOrderId(); processingLog.add(logEntry); System.out.println(logEntry); } } catch (InterruptedException e) { e.printStackTrace(); } }; // Start 3 customers Thread customer1 = new Thread(customerTask, "Alice"); Thread customer2 = new Thread(customerTask, "Bob"); Thread customer3 = new Thread(customerTask, "Charlie"); // Start 2 payment processors Thread processor1 = new Thread(paymentProcessor, "PaymentProcessor-1"); Thread processor2 = new Thread(paymentProcessor, "PaymentProcessor-2"); // Launch all threads customer1.start(); customer2.start(); customer3.start(); Thread.sleep(100); // Let customers start first processor1.start(); processor2.start(); // Wait for completion customer1.join(); customer2.join(); customer3.join(); processor1.join(); processor2.join(); // Generate report System.out.println("\n=== Order Processing Summary ==="); System.out.println("Total orders processed: " + processedOrders.size()); double totalRevenue = processedOrders.values().stream() .mapToDouble(Order::getAmount) .sum(); System.out.println("Total revenue: $" + String.format("%.2f", totalRevenue)); System.out.println("\nProcessed Orders:"); processedOrders.values().forEach(order -> { System.out.println(" " + order); }); System.out.println("\n=== System Statistics ==="); System.out.println("Log entries: " + processingLog.size()); System.out.println("Pending orders: " + pendingOrders.size()); System.out.println("\nAll operations completed safely with concurrent collections!"); }}/* Output:=== E-Commerce Order Processing System ===Alice placed Order#1001 [$45.67 - Alice - PENDING]Bob placed Order#1002 [$78.32 - Bob - PENDING]Charlie placed Order#1003 [$23.45 - Charlie - PENDING] PaymentProcessor-1 processing payment for Order#1001 [$45.67 - Alice - PENDING] PaymentProcessor-2 processing payment for Order#1002 [$78.32 - Bob - PENDING]Alice placed Order#1004 [$91.23 - Alice - PENDING] PaymentProcessor-1 completed payment: 1001 PaymentProcessor-1 processing payment for Order#1003 [$23.45 - Charlie - PENDING]Bob placed Order#1005 [$56.78 - Bob - PENDING] PaymentProcessor-2 completed payment: 1002 PaymentProcessor-2 processing payment for Order#1004 [$91.23 - Alice - PENDING]...=== Order Processing Summary ===Total orders processed: 9Total revenue: $523.45Processed Orders: Order#1001 [$45.67 - Alice - PAID] Order#1002 [$78.32 - Bob - PAID] Order#1003 [$23.45 - Charlie - PAID] ...=== System Statistics ===Log entries: 27Pending orders: 0All operations completed safely with concurrent collections!DEMONSTRATES:- BlockingQueue for order pipeline- ConcurrentHashMap for order tracking- CopyOnWriteArrayList for audit log- Real-world multi-stage processing*/Key Concepts
Thread-Safe vs Synchronized
Concurrent collections are internally thread-safe and optimized for concurrency. Synchronized wrappers (Collections.synchronizedList) lock the entire collection on every operation, making them much slower for concurrent access.
Copy-On-Write Pattern
CopyOnWriteArrayList/Set create a new copy on every modification. Expensive for writes, but reads are extremely fast with no locks. Perfect for scenarios with many readers and few writers (like event listeners).
Segment Locking
ConcurrentHashMap divides the map into segments and locks only the affected segment. This allows multiple threads to work on different segments simultaneously, providing high concurrency.
Blocking vs Non-Blocking
BlockingQueue blocks threads when full/empty (great for producer-consumer). ConcurrentLinkedQueue never blocks - uses lock-free algorithms for maximum throughput (great for high-speed message passing).
Best Practices
- ✓Use ConcurrentHashMap instead of Hashtable or Collections.synchronizedMap for better performance
- ✓Choose CopyOnWriteArrayList for read-heavy workloads with infrequent updates (like event listeners)
- ✓Use BlockingQueue for producer-consumer patterns - it handles all thread coordination automatically
- ✓For high-throughput scenarios, prefer ConcurrentLinkedQueue over synchronized alternatives
- ✓Avoid using size() or isEmpty() for logic decisions - these are approximate in concurrent collections
- ✓Use atomic operations like putIfAbsent, computeIfAbsent, merge instead of check-then-act patterns
Common Mistakes to Avoid
- ✗Using regular collections (ArrayList, HashMap) in multithreaded code without synchronization
- ✗Using CopyOnWriteArrayList for write-heavy workloads - each write copies the entire array!
- ✗Relying on size() method for logic in concurrent collections - size can change immediately after checking
- ✗Iterating over a concurrent collection and expecting no concurrent modifications - use iterators properly
- ✗Using synchronized wrappers when concurrent collections would be much faster
- ✗Not understanding that null values are not allowed in most concurrent collections
Interview Tips
- 💡Know the main concurrent collections: ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue, ConcurrentLinkedQueue
- 💡Understand when to use CopyOnWriteArrayList (many reads, few writes) vs synchronized list
- 💡Be able to explain how ConcurrentHashMap achieves high concurrency (segment locking/CAS operations)
- 💡Know the difference between BlockingQueue (blocks when full/empty) and ConcurrentLinkedQueue (never blocks)
- 💡Understand that fail-fast iterators don't work the same way in concurrent collections - they're weakly consistent
- 💡Be prepared to compare concurrent collections with synchronized wrappers and explain performance differences