In a multithreaded environment, working with shared data structures—like Java collections—can introduce serious problems if not handled carefully. When multiple threads access and modify a collection without synchronization, it can lead to unpredictable behavior, known as thread safety issues.
A collection is considered thread-safe if it behaves correctly when accessed from multiple threads, even when at least one of the threads is modifying it. Most standard Java collections, such as ArrayList
, HashMap
, and HashSet
, are not thread-safe. This means that concurrent modifications or iterations over them can result in race conditions, data corruption, and visibility problems.
A race condition occurs when multiple threads operate on the same data without proper synchronization, and the final outcome depends on the unpredictable order of execution. For example:
List<Integer> list = new ArrayList<>();
// Thread A
list.add(1);
// Thread B
list.remove(0);
If these operations happen at the same time, the internal structure of the ArrayList
can become inconsistent. Since ArrayList
is backed by an array, concurrent modifications might lead to incorrect indexing, missed updates, or even ArrayIndexOutOfBoundsException
.
Similarly, HashMap
is particularly vulnerable during concurrent updates. For instance, if two threads try to resize a HashMap
at the same time, it can enter an infinite loop, resulting in 100% CPU usage—an infamous issue prior to Java 8.
Even if modifications don’t immediately crash the program, the absence of proper synchronization can lead to visibility problems. One thread might not see the changes made by another thread, due to CPU-level caching or the Java Memory Model. For instance, after adding an element to a list, a second thread might still see the list as empty if changes haven’t been flushed to main memory.
Most collection classes in java.util
prioritize performance in single-threaded contexts. Their internal data structures are not guarded against concurrent access. They do not use synchronization or memory barriers, making them unsuitable for multithreaded access without external protection.
Map<String, Integer> map = new HashMap<>();
// Multiple threads accessing `map` can cause unpredictable behavior
One way to handle thread safety is by synchronizing access to collections manually using synchronized blocks:
synchronized (map) {
map.put("key", 1);
}
However, this quickly becomes error-prone and inefficient, especially under heavy contention. Moreover, manually synchronizing iteration and modification is complex.
To solve these problems, Java introduced concurrent collection classes in the java.util.concurrent
package. These collections are designed for safe access in concurrent environments without requiring external synchronization.
Examples include:
ConcurrentHashMap
— a thread-safe alternative to HashMap
CopyOnWriteArrayList
— safe for reading during modificationBlockingQueue
— supports thread coordination for producer-consumer problemsThese classes internally handle locking, memory visibility, and atomicity, making multithreaded programming safer and more manageable.
In summary, improper use of standard collections in concurrent scenarios can lead to severe issues like data corruption and race conditions. Developers must either manually synchronize access or, preferably, use purpose-built concurrent collections to ensure thread safety and program correctness.
Multithreaded programs often need to share and manipulate collections. However, traditional collections like ArrayList
and HashMap
are not thread-safe by default. Java’s java.util.concurrent
package introduces specialized collections designed to work efficiently and safely in concurrent environments. This section explains three of the most commonly used classes: ConcurrentHashMap
, CopyOnWriteArrayList
, and BlockingQueue
.
ConcurrentHashMap<K, V>
is a high-performance, thread-safe implementation of the Map
interface. It allows concurrent read and write operations without locking the entire map, making it suitable for high-concurrency scenarios like caching, counters, and real-time lookup services.
Prior to Java 8, ConcurrentHashMap
achieved concurrency through segment-based locking. The map was divided into multiple segments, and each segment could be locked independently. This allowed multiple threads to operate on different segments in parallel.
From Java 8 onward, ConcurrentHashMap
replaced segmented locking with a lock-free read strategy using volatile reads and CAS (Compare-And-Swap) for updates. Write operations use fine-grained synchronization on bins (buckets) instead of segments, and bins may be converted from linked lists to trees (similar to HashMap
) to improve performance in high-collision scenarios.
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
map.put("banana", 2);
// Safe concurrent access
Integer value = map.get("apple");
CopyOnWriteArrayList<E>
is a thread-safe variant of ArrayList
, optimized for scenarios with many reads and few writes. When an element is added, removed, or updated, the list creates a new internal array, leaving the existing one untouched for readers. This provides snapshot-style immutability to reading threads, avoiding synchronization overhead during reads.
Pros:
ConcurrentModificationException
.Cons:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
// Iteration is safe without locking
for (String item : list) {
System.out.println(item);
}
BlockingQueue<E>
is an interface representing a thread-safe queue that supports blocking operations. Unlike standard queues, its implementations allow producers to wait when the queue is full and consumers to wait when the queue is empty. This makes it ideal for producer-consumer patterns.
Popular implementations include:
ArrayBlockingQueue
– fixed capacity, array-backed.LinkedBlockingQueue
– optionally bounded, linked-node structure.PriorityBlockingQueue
– elements ordered by priority.put(E e)
– blocks if the queue is full.take()
– blocks if the queue is empty.offer(E e, long timeout, TimeUnit unit)
– waits up to a given timeout.poll(long timeout, TimeUnit unit)
– waits up to a given timeout for an element.BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// Producer thread
new Thread(() -> {
try {
queue.put("task");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// Consumer thread
new Thread(() -> {
try {
String item = queue.take();
System.out.println("Consumed: " + item);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
import java.util.concurrent.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ConcurrentCollectionsDemo {
public static void main(String[] args) throws InterruptedException {
// 1. ConcurrentHashMap
System.out.println("=== ConcurrentHashMap Example ===");
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
map.put("banana", 2);
// Safe concurrent access
Integer value = map.get("apple");
System.out.println("Value for 'apple': " + value);
// Concurrent update simulation
AtomicInteger counter = new AtomicInteger(3);
Runnable mapWriter = () -> {
for (int i = 0; i < 5; i++) {
map.put("key" + i, counter.getAndIncrement());
}
};
Thread t1 = new Thread(mapWriter);
Thread t2 = new Thread(mapWriter);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final ConcurrentHashMap: " + map);
// 2. CopyOnWriteArrayList
System.out.println("\n=== CopyOnWriteArrayList Example ===");
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
// Safe iteration
for (String item : list) {
System.out.println("List item: " + item);
}
// Add during iteration (won’t affect current iteration)
for (String item : list) {
list.add("C");
}
System.out.println("Final CopyOnWriteArrayList: " + list);
// 3. BlockingQueue
System.out.println("\n=== BlockingQueue Example ===");
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// Producer
Thread producer = new Thread(() -> {
try {
queue.put("task");
System.out.println("Produced: task");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Consumer
Thread consumer = new Thread(() -> {
try {
String item = queue.take();
System.out.println("Consumed: " + item);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
Collection | Strength | Best For |
---|---|---|
ConcurrentHashMap |
High-concurrency map operations | Real-time lookups, counters |
CopyOnWriteArrayList |
Fast, safe iteration; snapshot semantics | Read-heavy, infrequently updated lists |
BlockingQueue |
Built-in blocking support for threads | Producer-consumer coordination |
By choosing the right concurrent collection, you can simplify thread safety while maintaining performance and correctness in your Java applications.
In this section, we'll explore thread-safe operations using three core concurrent collections: ConcurrentHashMap
, CopyOnWriteArrayList
, and BlockingQueue
. These examples demonstrate how these collections handle concurrent access safely in multi-threaded environments.
import java.util.concurrent.*;
public class ConcurrentMapExample {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
map.merge("count", 1, Integer::sum); // Atomic update
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println("Final count: " + map.get("count")); // Should be 2000
}
}
Explanation: The merge()
method atomically updates the map. Two threads increment the same key, and ConcurrentHashMap
ensures thread safety without explicit synchronization.
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteExample {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A"); list.add("B"); list.add("C");
// Thread that modifies the list
new Thread(() -> list.add("D")).start();
// Safe iteration - works on a snapshot
for (String item : list) {
System.out.println("Reading: " + item);
}
}
}
Explanation: Unlike ArrayList
, CopyOnWriteArrayList
allows iteration while modifying the list concurrently. Readers see a consistent snapshot, avoiding ConcurrentModificationException
.
import java.util.concurrent.*;
public class BlockingQueueExample {
public static void main(String[] args) {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// Producer thread
new Thread(() -> {
try {
queue.put("Task 1");
queue.put("Task 2");
queue.put("Task 3");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// Consumer thread
new Thread(() -> {
try {
while (true) {
String task = queue.take(); // Blocks if empty
System.out.println("Processed: " + task);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
Explanation: The BlockingQueue
ensures smooth producer-consumer interaction. put()
blocks if the queue is full; take()
blocks if it's empty, making it ideal for task pipelines.
Collection | Thread-Safety Example | Notes |
---|---|---|
ConcurrentHashMap |
Concurrent counter update | Atomic operations using merge() |
CopyOnWriteArrayList |
Iteration with concurrent modification | Snapshot iteration avoids exceptions |
BlockingQueue |
Task producer-consumer with put /take |
Blocking behavior simplifies thread control |
These collections help eliminate race conditions and data corruption, offering built-in synchronization suited for common concurrent patterns in Java.