In this lesson, we will build a thread-safe bank account transfer system. You’ll learn how to use synchronization in Java to manage concurrent operations on shared resources like bank accounts, and how to prevent deadlocks using ordered resource acquisition.
By the end of this lesson, you will:
- Understand how to implement a thread-safe system for transferring money between bank accounts.
- Learn how to apply synchronization to protect shared resources.
- Explore how to avoid deadlock conditions by controlling the order of resource acquisition.
This lesson will guide you through writing Java code that safely transfers money between bank accounts while ensuring that no race conditions or deadlocks occur, even in a multi-threaded environment.
Imagine a banking system where clients transfer money between accounts concurrently. Without proper thread safety, multiple threads could attempt to modify the same account simultaneously, leading to race conditions and inconsistent balances.
To solve this, we use synchronization to control access to shared resources. When one thread transfers money between two accounts, it must ensure that no other thread can access those accounts until the transfer is complete.
The BankAccount
class represents a single account in the bank. We will synchronize the operations on this account to ensure that its state (balance) is not corrupted when accessed by multiple threads.
Java1public class BankAccount { 2 private final int id; 3 private int balance; 4 5 public BankAccount(int id, int balance) { 6 this.id = id; 7 this.balance = balance; 8 } 9 10 public int getId() { 11 return id; 12 }
Each BankAccount
object has an id
and a balance
. The id
is a unique identifier for the account, and the balance
keeps track of the money in the account. These fields are important for ensuring that transfers happen between the correct accounts and that balances are updated accurately.
Java1 public void deposit(int amount) { 2 balance += amount; 3 } 4 5 public boolean withdraw(int amount) { 6 if (balance < amount) return false; 7 balance -= amount; 8 return true; 9 }
The deposit
method adds money to the balance, and the withdraw
method subtracts money if sufficient funds are available. Both methods modify the balance
field, so we need to ensure that no two threads can change the balance at the same time. This will be handled by synchronization in the transfer method.
Java1 public static void transfer(BankAccount from, BankAccount to, int amount) { 2 BankAccount first = from.getId() < to.getId() ? from : to; 3 BankAccount second = from.getId() < to.getId() ? to : from;
The first step in the transfer
method is preventing deadlock by ensuring that locks on the BankAccount
objects are acquired in a consistent order. We do this by comparing the IDs of the accounts involved in the transfer. The account with the lower ID is always locked first, followed by the account with the higher ID.
This approach prevents circular waiting, which is a condition that can lead to deadlock. By always acquiring locks in a consistent order, we ensure that no deadlock will occur.
Java1 synchronized (first) { 2 3 try { 4 Thread.sleep(100); // Simulating some work 5 } catch (InterruptedException e) { 6 Thread.currentThread().interrupt(); 7 } 8 9 synchronized (second) { 10 if (from.withdraw(amount)) { 11 to.deposit(amount); 12 System.out.println("Transferred " + amount + " from Account " + from.getId() + " to Account " + to.getId()); 13 } else { 14 System.out.println("Insufficient funds in Account " + from.getId()); 15 } 16 } 17 } 18 } 19}
Once the locking order is determined, we use synchronized blocks to protect the withdraw
and deposit
operations. A synchronized
block ensures that only one thread can execute the code within the block at any given time for the object being synchronized.
In this case, the first synchronized
block locks the first
bank account (the one with the lower ID), and the second synchronized
block locks the second
bank account. By using nested synchronization, we guarantee that no two threads can simultaneously modify the same accounts during a transfer.
If the withdraw
operation is successful (i.e., there are sufficient funds in the from
account), the deposit
method is called on the to
account. If there aren’t enough funds, a message indicating insufficient funds is printed. The locks are automatically released when the synchronized blocks complete.
In the Main
class, we simulate the transfer system by creating two accounts and using multiple threads to perform transfers concurrently.
Java1public class Main { 2 public static void main(String[] args) { 3 BankAccount account1 = new BankAccount(1, 500); 4 BankAccount account2 = new BankAccount(2, 500);
We create two bank accounts with initial balances of 500. These accounts will be used in the transfer operations.
Java1 Thread t1 = new Thread(() -> { 2 for (int i = 0; i < 10; i++) { 3 BankAccount.transfer(account1, account2, 10); 4 } 5 }); 6 7 Thread t2 = new Thread(() -> { 8 for (int i = 0; i < 10; i++) { 9 BankAccount.transfer(account2, account1, 10); 10 } 11 }); 12 13 t1.start(); 14 t2.start(); 15 } 16}
We start two threads, t1
and t2
, that perform multiple transfers between the two accounts. Thread t1
transfers from account1
to account2
, and thread t2
transfers from account2
to account1
. Both threads run concurrently, testing the thread safety of the transfer
method.
By running this program, you can observe how synchronization ensures that transfers are performed safely and that no race conditions or deadlocks occur.
Building a thread-safe bank account transfer system is a valuable skill in concurrent programming. It ensures that data remains consistent, prevents race conditions, and avoids deadlocks, which are crucial for creating robust, high-performance applications.
- Data consistency: Synchronization guarantees that the account balance is always correct, even with concurrent access.
- Deadlock prevention: Using an ordered locking mechanism prevents deadlocks from occurring.
- Concurrency: This system allows multiple threads to safely interact with the same shared resources (bank accounts).
Now, let's move to the practice section to see everything all together!