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.
