├── README.md └── src └── com └── assignment ├── AccountRunnable.java ├── BankAccount.java ├── LockAccount.java ├── Main.java └── TransactionalAccount.java /README.md: -------------------------------------------------------------------------------- 1 | BankAccountExample 2 | ================== 3 | 4 | Java multithreaded bank account example 5 | 6 | We need locks because we have a shared mutable variable (available funds). With no locks this could happen:
7 | Thread 1 : Account source has $100 avail funds wants to transfer to Account destination $30
8 | Thread 2 : Account source has $100 avail funds wants to transfer to Account destination $80
9 | Thread 1 : Checks if source has >= amount, condition is true then for some reason halts
10 | Thread 2 : Available funds are still at $100 so the condition is true and goes ahead and transfers the money
11 | Thread 1 : Resumes and since it already passed the condition it will deduct the $30 from account x - putting it at a negative amount
12 | 13 | Need to add a lock to safely update the mutable object, but might not be as straightforward 14 | 15 | This case:
16 | Thread1: sourceAccount.transferFunds(destinationAccount, 10)
17 | Thread2: destinationAccount.transferFunds(sourceAccount, 20)
18 | 19 | Solution 1:
20 | transferFunds could be declared as a synchronized function, but that's not Atomic and locks the whole function 21 | 22 | Solution 2:
23 | Lock on the source and destination account, so no simultaneous updates happen 24 | 25 | Even with those two locks deadlock could happen in this situation. Why? 26 | 27 | Thread 1: Obtains lock on source account
28 | Thread 2: Obtains lock on destination account
29 | Thread 1: Asks for lock on destination account but Thread 2 still has it
30 | Thread 2: Asks for lock on source account but Thread 1 still has it
31 | DEADLOCK
32 | 33 | -- one solution to the problem would be to provide a global lock object this would solve the deadlock problem but again not ideal
34 | -- what if I use the account id to enforce a lock order? This would prevent the deadlock
35 | 36 | Another completely different approach which I implemented is using Multiverse's implementation of transactions. Using STM instead of locks ensures 37 | atomic behavior and prevents deadlocks. 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/com/assignment/AccountRunnable.java: -------------------------------------------------------------------------------- 1 | package com.assignment; 2 | 3 | /** 4 | * AccountRunnable class 5 | * Created by saba on 2014-10-11. 6 | */ 7 | public class AccountRunnable implements Runnable { 8 | private T sourceBankAccount; 9 | private T destBankAccount; 10 | private int transferAmount; 11 | private String threadName; 12 | 13 | 14 | AccountRunnable(T source, T destination, int amount, String name) { 15 | this.sourceBankAccount = source; 16 | this.destBankAccount = destination; 17 | this.transferAmount = amount; 18 | this.threadName = name; 19 | } 20 | 21 | public void run() { 22 | System.out.println(threadName + " starting transfer from " + sourceBankAccount.getId() + " to " + destBankAccount.getId()); 23 | //need to check for types somehow 24 | 25 | sourceBankAccount.transferFunds(destBankAccount, transferAmount); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/com/assignment/BankAccount.java: -------------------------------------------------------------------------------- 1 | package com.assignment; 2 | 3 | /** 4 | * Created by saba on 2014-10-11. 5 | */ 6 | public interface BankAccount { 7 | public long getId(); 8 | public void transferFunds(final T destination, int amount); 9 | public void printAvailableFunds(); 10 | } 11 | -------------------------------------------------------------------------------- /src/com/assignment/LockAccount.java: -------------------------------------------------------------------------------- 1 | package com.assignment; 2 | 3 | /** 4 | * Bank Account class using ordered locks 5 | * Created by saba on 2014-10-11. 6 | */ 7 | public class LockAccount implements BankAccount { 8 | private Long id; 9 | 10 | //mutable state, yay 11 | private int availableFunds; 12 | 13 | //Constructor 14 | public LockAccount(long accountId, int accountAmount) { 15 | setId(accountId); 16 | setAvailableFunds(accountAmount); 17 | } 18 | 19 | // Getters and setters for the above fields. 20 | public long getId() { 21 | return this.id; 22 | } 23 | 24 | public void setId(long accountId){ 25 | this.id = accountId; 26 | } 27 | 28 | public int getAvailableFunds() { 29 | return this.availableFunds; 30 | } 31 | 32 | public void setAvailableFunds(int amount){ 33 | this.availableFunds = amount; 34 | } 35 | 36 | private void withdraw(int amount) { 37 | try { 38 | Thread.sleep(1000L); //simulating DB access 39 | } catch(InterruptedException e) {} 40 | availableFunds -= amount; 41 | } 42 | 43 | private void deposit(int amount) { 44 | try { 45 | Thread.sleep(100L); //simulating DB access 46 | } catch(InterruptedException e) {} 47 | availableFunds += amount; 48 | } 49 | 50 | private int compareAccountIds(LockAccount destination) { 51 | return this.id.compareTo(destination.id); 52 | } 53 | 54 | public void printAvailableFunds() { 55 | System.out.println("Account with id " + this.id + " has " + this.availableFunds); 56 | } 57 | /** 58 | * I broke the transferFunds function into deposit and withdraw 59 | * Lock ordering by account ids - prevents deadlock when two threads are trying 60 | * to update bank accounts of reverse order 61 | * @param destination destination account 62 | * @param amount amount of money to transfer 63 | */ 64 | public void transferFunds(final LockAccount destination, int amount) { 65 | LockAccount sourceLock, destLock; 66 | 67 | int result = compareAccountIds(destination); 68 | //destination account id is greater than source account id 69 | if (result > 0) { 70 | sourceLock = destination; 71 | destLock = this; 72 | } else { 73 | sourceLock = this; 74 | destLock = destination; 75 | } 76 | System.out.println("Source lock id " + sourceLock.getId()); 77 | System.out.println("Destination lock id " + destLock.getId()); 78 | synchronized (sourceLock) { 79 | System.out.println("Source account lock obtained"); 80 | if (this.availableFunds <= amount) { 81 | throw new IllegalArgumentException("Insufficient funds in source account with ID: " + this.id); 82 | } 83 | this.withdraw(amount); 84 | synchronized (destLock) { 85 | System.out.println("Destination account lock obtained"); 86 | destination.deposit(amount); 87 | } 88 | 89 | } 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/com/assignment/Main.java: -------------------------------------------------------------------------------- 1 | package com.assignment; 2 | 3 | public class Main { 4 | 5 | public static void main(String[] args) { 6 | 7 | //Two example accounts using locks 8 | LockAccount sourceLockAccount = new LockAccount(1L, 100); 9 | LockAccount destinationLockAccount = new LockAccount(2L, 80); 10 | 11 | Thread t1 = new Thread(new AccountRunnable(sourceLockAccount, destinationLockAccount, 10, "Thread1")); 12 | Thread t2 = new Thread(new AccountRunnable(destinationLockAccount, sourceLockAccount, 20, "Thread2")); 13 | Thread t3 = new Thread(new AccountRunnable(destinationLockAccount, sourceLockAccount, 5, "Thread3")); 14 | 15 | t1.start(); 16 | t2.start(); 17 | t3.start(); 18 | 19 | try { 20 | t1.join(); 21 | t2.join(); 22 | t3.join(); 23 | } catch(InterruptedException e){} 24 | 25 | System.out.println("All threads are done"); 26 | sourceLockAccount.printAvailableFunds(); 27 | destinationLockAccount.printAvailableFunds(); 28 | 29 | 30 | //Two example accounts using transactions (multiverse's implementation of STM) 31 | TransactionalAccount sourceTxnAccount = new TransactionalAccount(1L, 100); 32 | TransactionalAccount destTxnAccount = new TransactionalAccount(2L, 80); 33 | 34 | t1 = new Thread(new AccountRunnable(sourceTxnAccount, destTxnAccount, 10, "Thread1")); 35 | t2 = new Thread(new AccountRunnable(destTxnAccount, sourceTxnAccount, 20, "Thread2")); 36 | t3 = new Thread(new AccountRunnable(destTxnAccount, sourceTxnAccount, 5, "Thread3")); 37 | 38 | t1.start(); 39 | t2.start(); 40 | t3.start(); 41 | 42 | try { 43 | t1.join(); 44 | t2.join(); 45 | t3.join(); 46 | } catch(InterruptedException e){} 47 | 48 | System.out.println("All threads are done"); 49 | sourceTxnAccount.printAvailableFunds(); 50 | destTxnAccount.printAvailableFunds(); 51 | 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/com/assignment/TransactionalAccount.java: -------------------------------------------------------------------------------- 1 | package com.assignment; 2 | import org.multiverse.api.references.*; 3 | import static org.multiverse.api.StmUtils.*; 4 | 5 | /** 6 | * Bank Account class using transactions instead of locks 7 | * Created by saba on 2014-10-11. 8 | */ 9 | public class TransactionalAccount implements BankAccount { 10 | 11 | private TxnInteger availableFunds; 12 | private long id; 13 | 14 | public TransactionalAccount(long accountId, int balance){ 15 | this.availableFunds = newTxnInteger(balance); 16 | this.id = accountId; 17 | } 18 | 19 | // Getters and setters for the above fields. 20 | public long getId() { 21 | return this.id; 22 | } 23 | 24 | public void setId(long accountId){ 25 | this.id = accountId; 26 | } 27 | 28 | public TxnInteger getAvailableFunds() { 29 | return this.availableFunds; 30 | } 31 | 32 | public void setAvailableFunds(TxnInteger amount){ 33 | this.availableFunds = amount; 34 | } 35 | 36 | private void withdraw(int amount) { 37 | try { 38 | Thread.sleep(1000L); //simulating DB access 39 | } catch(InterruptedException e) {} 40 | this.availableFunds.decrement(amount); 41 | } 42 | 43 | private void deposit(int amount) { 44 | try { 45 | Thread.sleep(100L); //simulating DB access 46 | } catch(InterruptedException e) {} 47 | this.availableFunds.increment(amount); 48 | } 49 | 50 | public void printAvailableFunds() { 51 | System.out.println("Account with id " + this.id + " has " + this.availableFunds.atomicToString()); 52 | } 53 | 54 | public void transferFunds(final TransactionalAccount destination,final int amount) { 55 | final TransactionalAccount source = this; 56 | atomic(new Runnable() { 57 | @Override 58 | public void run() { 59 | source.withdraw(amount); 60 | destination.deposit(amount); 61 | } 62 | }); 63 | } 64 | } 65 | --------------------------------------------------------------------------------