Java provides several utilities for managing synchronization in collections to ensure thread safety when multiple threads are accessing or modifying them concurrently. Two primary ways to achieve this are using explicit locking with ReentrantLock and using synchronized collections from the java.util.Collections class.
Locking with ReentrantLock
ReentrantLock provides explicit control over locking, offering flexibility and capabilities like fairness policies and interruptible lock acquisition.
Program Implementation
Reader/Writer Problem
The Reader/Writer problem is a classic synchronization problem in computer science, which involves controlling access to a shared resource (e.g., a database or file) that is accessed by multiple readers and writers.
-
Readers can read the resource concurrently as long as no writer is writing to the resource.
-
Writers must have exclusive access to the resource, meaning that no other reader or writer can access the resource while a writer is writing.
//LockDemo.java import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockDemo { private final List<Integer> list = new ArrayList<>(); private final Lock lock = new ReentrantLock(); public void addItem(int item) { lock.lock(); try { list.add(item); } finally { lock.unlock(); } } public int getItem(int index) { lock.lock(); try { return list.get(index); } finally { lock.unlock(); } } public static void main(String[] args) { LockDemo ld = new LockDemo(); Runnable writerTask = () -> { for (int i = 0; i < 10; i++) { ld.addItem(i); System.out.println(Thread.currentThread().getName() + " added " + i); } }; Runnable readerTask = () -> { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " read " + ld.getItem(i)); } }; Thread writerThread1 = new Thread(writerTask, "Writer-1"); Thread writerThread2 = new Thread(writerTask, "Writer-2"); Thread readerThread = new Thread(readerTask, "Reader"); writerThread1.start(); writerThread2.start(); try { writerThread1.join(); writerThread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } readerThread.start(); } } /* C:\>javac LockDemo.java C:\>java LockDemo Writer-2 added 0 Writer-1 added 0 Writer-2 added 1 Writer-1 added 1 Writer-2 added 2 Writer-2 added 3 Writer-2 added 4 Writer-2 added 5 Writer-2 added 6 Writer-2 added 7 Writer-2 added 8 Writer-1 added 2 Writer-2 added 9 Writer-1 added 3 Writer-1 added 4 Writer-1 added 5 Writer-1 added 6 Writer-1 added 7 Writer-1 added 8 Writer-1 added 9 Reader read 0 Reader read 0 Reader read 1 Reader read 1 Reader read 2 Reader read 2 Reader read 3 Reader read 4 Reader read 5 Reader read 6 */
Synchronized Collections
Java’s Collections utility class provides factory methods to create synchronized collections. These collections are thread-safe as all the operations are wrapped with synchronization.
From java.util.Collections class
import java.util.*;
// Synchronized List
public static <T> List<T> synchronizedList(List<T> list) {
return Collections.synchronizedList(list);
}
// Synchronized Set
public static <T> Set<T> synchronizedSet(Set<T> set) {
return Collections.synchronizedSet(set);
}
// Synchronized Map
public static <K, V> Map<K, V> synchronizedMap(Map<K, V> map) {
return Collections.synchronizedMap(map);
}
// Synchronized Sorted Set
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> set) {
return Collections.synchronizedSortedSet(set);
}
// Synchronized Sorted Map
public static <K, V> SortedMap<K, V> synchronizedSortedMap(SortedMap<K, V> map) {
return Collections.synchronizedSortedMap(map);
}
Code language: JavaScript (javascript)
//SynchronizedCollectionsDemo.java import java.util.ArrayList; import java.util.Collections; import java.util.List; public class SynchronizedCollectionsDemo { private final List<Integer> list = Collections.synchronizedList(new ArrayList<>()); public void addItem(int item) { list.add(item); } public int getItem(int index) { synchronized (list) { return list.get(index); } } public static void main(String[] args) { SynchronizedCollectionsDemo demo = new SynchronizedCollectionsDemo(); Runnable writerTask = () -> { for (int i = 0; i < 10; i++) { demo.addItem(i); System.out.println(Thread.currentThread().getName() + " added " + i); } }; Runnable readerTask = () -> { synchronized (demo.list) { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " read " + demo.getItem(i)); } } }; Thread writerThread1 = new Thread(writerTask, "Writer-1"); Thread writerThread2 = new Thread(writerTask, "Writer-2"); Thread readerThread = new Thread(readerTask, "Reader"); writerThread1.start(); writerThread2.start(); try { writerThread1.join(); writerThread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } readerThread.start(); } } /* C:\>javac SynchronizedCollectionsDemo.java C:\>java SynchronizedCollectionsDemo Writer-2 added 0 Writer-1 added 0 Writer-2 added 1 Writer-1 added 1 Writer-2 added 2 Writer-1 added 2 Writer-2 added 3 Writer-1 added 3 Writer-2 added 4 Writer-1 added 4 Writer-2 added 5 Writer-1 added 5 Writer-2 added 6 Writer-1 added 6 Writer-2 added 7 Writer-1 added 7 Writer-2 added 8 Writer-1 added 8 Writer-2 added 9 Writer-1 added 9 Reader read 0 Reader read 0 Reader read 1 Reader read 1 Reader read 2 Reader read 2 Reader read 3 Reader read 3 Reader read 4 Reader read 4 */
In summary ReentrantLock provides explicit locking control, useful for more complex locking mechanisms and fairness policies and synchronized collections simplifies synchronization by providing thread-safe collections with internal synchronization.
Both methods ensure thread safety and prevent concurrent modification issues, but they offer different levels of control and complexity. Use ReentrantLock for advanced locking needs and synchronized collections for simpler thread-safe collection operations.