Week 155 — What thread-safe collections does Java provide?

Question of the Week #155
What thread-safe collections does Java provide?
3 Replies
MrMisha | Coder
To help with writing multithreaded code, Java provides many mechanisms for synchronization and sharing data between different threads. Among others, these include different types of locks, atomic types, Semaphore, CountDownLatch, CyclicBarriers and some thread-safe collections. One commonly used thread-safe collection is LinkedBlockingQueue. This class is a thread-safe implementation of the Queue (and BlockingQueue) interface that allows inserting elements on one side and retrieving elements on the other side. These operations can be executed by any number of threads and Java ensures that the collection is in a consistent state with the elements being inserted in the correct order. LinkedBlockingQueues can be created with a fixed capacity allowing elements to be inserted if and only if there is sufficient space in it. When inserting an element into a full LinkedBlockingQueue with put(element), the operation will wait until another thread removes an element from it. Similarly, when attempting to remove an element from an empty LinkedBlockingQueue with take(), it will wait until an element is added (by another thread). LinkedBlockingQueues are commonly used for producer-consumer scenarios.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
for(int i = 0; i < 2; i++) {
// create a producer
new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
try {
long millis = ThreadLocalRandom.current().nextLong(100, 1000);
TimeUnit.MILLISECONDS.sleep(millis);
queue.put("This element took " + millis + "ms to produce.");
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
for(int i = 0; i < 4; i++) {
// create a consumer
new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
try {
String element = queue.take();
System.out.println("consuming element: " + element);
TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(200, 2000));
System.out.println("element consumed: " + element);
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
for(int i = 0; i < 2; i++) {
// create a producer
new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
try {
long millis = ThreadLocalRandom.current().nextLong(100, 1000);
TimeUnit.MILLISECONDS.sleep(millis);
queue.put("This element took " + millis + "ms to produce.");
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
for(int i = 0; i < 4; i++) {
// create a consumer
new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
try {
String element = queue.take();
System.out.println("consuming element: " + element);
TimeUnit.MILLISECONDS.sleep(ThreadLocalRandom.current().nextLong(200, 2000));
System.out.println("element consumed: " + element);
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
In addition to that, Java also provides a LinkedBlockingDeque class which is similar to LinkedBlockingQueue but implements the Deque interface (as well as BlockingDeque, Queue and BlockingQueue) and works in a similar way to LinkedBlockingQueue except that it supports adding and retrieving elements from both sides. In addition to Queues and Deques, Java also allows creating synchronized collections from other backing collections using the factory methods prefixed with synchronized in the Collections class. To do that, first create a backing collection and then pass it to the synchronized collection factory method. The collection returned by the factory will be thread-safe as long as the backing collection is not accessed after the synchronized collection is created (accesses using the synchronized collection are fine). These "synchronized collections" work by forwarding accesses to the backing collection but preventing concurrent access by requiring accesses to wait until the previous access finishes.
List<String> synchronizedArrayList = Collections.synchronizedList(new ArrayList<>());
Set<String> synchronizedHashSet = Collections.synchronizedSet(new HashSet<>());
Map<String, String> synchronizedLinkedHashMap = Collections.synchronizedMap(new LinkedHashMap<>());
SortedMap<String, String> synchronizedTreeMap = Collections.synchronizedSortedMap(new TreeMap<>());
// ...
List<String> synchronizedArrayList = Collections.synchronizedList(new ArrayList<>());
Set<String> synchronizedHashSet = Collections.synchronizedSet(new HashSet<>());
Map<String, String> synchronizedLinkedHashMap = Collections.synchronizedMap(new LinkedHashMap<>());
SortedMap<String, String> synchronizedTreeMap = Collections.synchronizedSortedMap(new TreeMap<>());
// ...
MrMisha | Coder
In many situations, it's also useful to have a performant thread-safe Map for e.g. caching in multithreaded environments. For these use-cases, Java provides ConcurrentHashMap which is a high-performance thread-safe Map which provides nonblocking read operations.
Map<String, String> cache = new ConcurrentHashMap<>();
Map<String, String> cache = new ConcurrentHashMap<>();
// lookup cacheKey or produce an element and insert it if it doesn't exist
cache.computeIfAbsent(cacheKey, k -> produceElement(k));
// lookup cacheKey or produce an element and insert it if it doesn't exist
cache.computeIfAbsent(cacheKey, k -> produceElement(k));
In some cases (e.g. event listeners which are often retrieved/iterated over but rarely changed), one needs a thread-safe List that provides fast read access and is rarely modified. For that, Java comes with the CopyOnWriteArrayList class which is an array-based list that creates a copy of the entire internal array on every modification and switches to the copy of the array once the array is copied. As read accesses are not much more than simple array accesses, these can be performed very quickly but this comes at the cost of slow write accesses, especially if the collection is fairly big. Note that these are just some of the thread-safe collections Java provides. There are also others which are useful for different needs.
📖 Sample answer from dan1st
MrMisha | Coder
To use thread-safe collections in Java: Collections.synchronizedX() — Synchronizes an existing collection (e.g., List, Map). java.util.concurrent — New and efficient collections: ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue, and others. Modern java.util.concurrent collections are more recommended.
Submission from mrmisha001

Did you find this page helpful?