Java provides several utilities and collections that help in writing thread-safe code. These utilities and collections are part of the java.util.concurrent package.
Thread Safety Utilities
Locks and Synchronizers
ReentrantLock: Provides a re-entrant mutual exclusion lock with the same basic behaviour and semantics as the implicit monitor lock accessed using synchronized methods and statements but with extended capabilities.
ReadWriteLock: Maintains a pair of associated locks, one for read-only operations and one for write operations. The read lock may be held simultaneously by multiple reader threads as long as there are no writers.
Countdown Latch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
Cyclic Barrier: A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.
Semaphore: A counting semaphore that can be used to control access to a resource with a fixed number of permits.
Exchanger: A synchronization point at which threads can pair and swap elements within pairs.
Executor Framework
ExecutorService: A higher-level replacement for working with threads directly. Allows managing a pool of threads.
ScheduledExecutorService: A service that can schedule commands to run after a given delay or to execute periodically.
Thread-Safe Collections
Blocking Queues
ArrayBlockingQueue: A bounded blocking queue backed by an array.
LinkedBlockingQueue: An optionally bounded blocking queue based on linked nodes.
PriorityBlockingQueue: An unbounded blocking queue that uses the same ordering rules as PriorityQueue.
DelayQueue: A time-based scheduling queue where elements cannot be accessed until a certain delay has expired.
SynchronousQueue: A blocking queue in which each insert operation must wait for a corresponding remove operation by another thread and vice versa.
Concurrent Collections
ConcurrentHashMap: A hash table supporting full concurrency of retrievals and adjustable expected concurrency for updates.
ConcurrentSkipListMap: A scalable concurrent NavigableMap implementation.
ConcurrentSkipListSet: A scalable concurrent NavigableSet implementation.
CopyOnWriteArrayList: A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.
CopyOnWriteArraySet: A thread-safe variant of HashSet that uses an internal CopyOnWriteArrayList for all its operations.
Program
Demonstrating the use of ConcurrentHashMap and ReentrantLock to handle thread-safe operations in a banking scenario.
//Bank.java import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; public class Bank { private final ConcurrentHashMap<String, Double> accounts = new ConcurrentHashMap<>(); private final ReentrantLock lock = new ReentrantLock(); public void addAccount(String accountId, double initialBalance) { accounts.put(accountId, initialBalance); } public void deposit(String accountId, double amount) { lock.lock(); try { accounts.computeIfPresent(accountId, (id, balance) -> balance + amount); System.out.println(Thread.currentThread().getName() + " deposited " + amount + " to account " + accountId + ". New balance: " + accounts.get(accountId)); } finally { lock.unlock(); } } public void withdraw(String accountId, double amount) { lock.lock(); try { accounts.computeIfPresent(accountId, (id, balance) -> { if (balance >= amount) { return balance - amount; } else { System.out.println(Thread.currentThread().getName() + " tried to withdraw " + amount + " from account " + accountId + " but insufficient balance. Current balance: " + balance); return balance; } }); System.out.println(Thread.currentThread().getName() + " withdrew " + amount + " from account " + accountId + ". New balance: " + accounts.get(accountId)); } finally { lock.unlock(); } } public double getBalance(String accountId) { return accounts.getOrDefault(accountId, 0.0); } public static void main(String[] args) { Bank bank = new Bank(); bank.addAccount("12345", 1000); Runnable depositor = () -> { for (int i = 0; i < 5; i++) { bank.deposit("12345", 100); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }; Runnable withdrawer = () -> { for (int i = 0; i < 5; i++) { bank.withdraw("12345", 50); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread depositorThread1 = new Thread(depositor, "DepositorThread1"); Thread depositorThread2 = new Thread(depositor, "DepositorThread2"); Thread withdrawerThread1 = new Thread(withdrawer, "WithdrawerThread1"); Thread withdrawerThread2 = new Thread(withdrawer, "WithdrawerThread2"); depositorThread1.start(); depositorThread2.start(); withdrawerThread1.start(); withdrawerThread2.start(); try { depositorThread1.join(); depositorThread2.join(); withdrawerThread1.join(); withdrawerThread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Final balance: " + bank.getBalance("12345")); } } /* C:\>javac Bank.java C:\>java Bank DepositorThread1 deposited 100.0 to account 12345. New balance: 1100.0 DepositorThread2 deposited 100.0 to account 12345. New balance: 1200.0 WithdrawerThread2 withdrew 50.0 from account 12345. New balance: 1150.0 WithdrawerThread1 withdrew 50.0 from account 12345. New balance: 1100.0 DepositorThread1 deposited 100.0 to account 12345. New balance: 1200.0 DepositorThread2 deposited 100.0 to account 12345. New balance: 1300.0 DepositorThread2 deposited 100.0 to account 12345. New balance: 1400.0 DepositorThread1 deposited 100.0 to account 12345. New balance: 1500.0 WithdrawerThread2 withdrew 50.0 from account 12345. New balance: 1450.0 WithdrawerThread1 withdrew 50.0 from account 12345. New balance: 1400.0 DepositorThread2 deposited 100.0 to account 12345. New balance: 1500.0 WithdrawerThread1 withdrew 50.0 from account 12345. New balance: 1450.0 WithdrawerThread2 withdrew 50.0 from account 12345. New balance: 1400.0 DepositorThread1 deposited 100.0 to account 12345. New balance: 1500.0 DepositorThread1 deposited 100.0 to account 12345. New balance: 1600.0 DepositorThread2 deposited 100.0 to account 12345. New balance: 1700.0 WithdrawerThread2 withdrew 50.0 from account 12345. New balance: 1650.0 WithdrawerThread1 withdrew 50.0 from account 12345. New balance: 1600.0 WithdrawerThread1 withdrew 50.0 from account 12345. New balance: 1550.0 WithdrawerThread2 withdrew 50.0 from account 12345. New balance: 1500.0 Final balance: 1500.0 */
Java offers a rich set of thread safety utilities and thread-safe collections to simplify multi-threaded programming and avoid concurrency issues. Utilities like Semaphore
, CountDownLatch
, CyclicBarrier
, and Exchanger
help coordinate and control thread execution efficiently. Meanwhile, thread-safe collections like Vector
, Hashtable
, and classes from java.util.concurrent
(ConcurrentHashMap
, CopyOnWriteArrayList
, etc.) provide built-in mechanisms for safe access and modification across multiple threads without needing explicit synchronization.
By using these utilities and collections appropriately, developers can build high-performance, scalable, and robust multi-threaded applications with significantly reduced risk of data corruption, deadlocks, and race conditions.