2. Java Multithreading: Synchronization, Volatile Variables, and Atomic Operations
An Advance guide to using synchronization, volatile variables, and atomic operations with examples
What is Synchronization?
In Java, the synchronized keyword is used to create a critical section that allows only one thread to execute a block of code at a time. This prevents data corruption and inconsistent results when multiple threads are trying to read or write shared data and Keeping Things in Order.
Example Scenario:
You need to manage a bank account where multiple threads can deposit and withdraw money. To avoid inconsistent balances, you use synchronization.
public class BankAccount {
private int balance = 0;
public synchronized void deposit(int amount) {
balance += amount;
System.out.println("Deposit Amount: " + amount);
}
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
System.out.println("Withdraw Amount: " + amount);
} else {
System.out.println("Insufficient Amount...");
}
}
public synchronized int getBalance() {
return balance;
}
public static void main(String[] args) {
BankAccount account = new BankAccount();
// Create threads to deposit and withdraw money
new Thread(() -> account.deposit(100)).start();
new Thread(() -> account.withdraw(50)).start();
new Thread(() -> account.deposit(200)).start();
// Allow some time for threads to finish
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final Balance: " + account.getBalance());
}
}
The synchronized keyword ensures that only one thread can execute the deposit, withdraw, or getBalance methods at a time, preventing data corruption.
What are Volatile Variables?
A volatile variable ensures that any change made to it by one thread is visible to all other threads immediately. This is useful for flags or status indicators.
Example Scenario: You want to manage a simple lock flag for operations on a bank account. Using volatile ensures that the lock status is visible across all threads immediately.
public class AccountLockExample {
private volatile boolean lock = false;
public void acquireLock() {
lock = true;
}
public void releaseLock() {
lock = false;
}
public boolean isLocked() {
return lock;
}
public static void main(String[] args) {
AccountLockExample accountLock = new AccountLockExample();
// Start a thread to acquire the lock
new Thread(() -> {
accountLock.acquireLock();
System.out.println("Lock acquired.");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountLock.releaseLock();
System.out.println("Lock released.");
}).start();
// Check the lock status from the main thread
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Lock status: " + (accountLock.isLocked() ? "Locked" : "Unlocked"));
}
}
The volatile keyword ensures that the lock status is updated and visible across all threads immediately, allowing for simple lock management.
What are Atomic Variables:
Atomic variables use special low-level instructions to ensure that operations on them are atomic and thread-safe.
Example Scenario: You want to keep track of the number of transactions performed on a bank account using multiple threads. AtomicInteger helps manage this safely.
import java.util.concurrent.atomic.AtomicInteger;
public class TransactionCounter {
private final AtomicInteger transactionCount = new AtomicInteger(0);
public void incrementTransaction() {
// Atomically increment the transaction count
transactionCount.incrementAndGet();
}
public int getTransactionCount() {
// Atomically get the current count
return transactionCount.get();
}
public static void main(String[] args) {
TransactionCounter counter = new TransactionCounter();
// Start threads to increment the transaction count
for (int i = 0; i < 10; i++) {
new Thread(counter::incrementTransaction).start();
}
// Allow some time for threads to finish
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Total Transactions: " + counter.getTransactionCount());
}
}
AtomicInteger provides incrementAndGet() to safely increment the transaction count without needing explicit synchronization.
Conclusion:
These examples help to better understand, how to use synchronization, volatile variables, and atomic operations in a multithreaded environment. By applying these concepts, you can ensure data consistency and safety in your applications.
Complete Code on GitHub
If you found this post helpful, consider subscribing for more updates or following me on LinkedIn, and GitHub. Have questions or suggestions? Feel free to leave a comment or contact us. Happy coding!!