diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..812c3f9 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..eba6e1f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/java/dev/banking/model/BankAccount.java b/src/main/java/dev/banking/model/BankAccount.java index 745ede2..aca9814 100644 --- a/src/main/java/dev/banking/model/BankAccount.java +++ b/src/main/java/dev/banking/model/BankAccount.java @@ -1,18 +1,34 @@ package dev.banking.model; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Thread-safe BankAccount implementation. + * + * Design choices: + * - Each account has its OWN ReentrantLock (per-account locking). + * This means two threads working on DIFFERENT accounts never block each other. + * + * - deposit() and withdraw() simply acquire this account's lock, + * modify the balance, then release the lock. Always inside try/finally + * so the lock is GUARANTEED to be released even if an exception occurs. + * + * - getBalance() also acquires the lock so it never reads a half-written value. + * + * - transfer() is the most complex: + * It must lock TWO accounts at the same time. + * To prevent deadlock we ALWAYS lock the account with the LOWER accountId first. + * Example: Thread 1 does A(id=1) -> B(id=2) → locks id=1 first, then id=2 + * Thread 2 does B(id=2) -> A(id=1) → ALSO locks id=1 first, then id=2 + * Because both threads always lock in the same order, they can never deadlock. + */ public class BankAccount { private final int accountId; private long balance; - /* - * Students may introduce additional fields - * such as: - * - Lock / ReentrantLock - * - ReadWriteLock - * - Object monitor - * - etc. - */ + // One lock per account instance. Never shared with other accounts. + private final ReentrantLock lock = new ReentrantLock(); public BankAccount(int accountId, long initialBalance) { this.accountId = accountId; @@ -23,56 +39,93 @@ public class BankAccount { return accountId; } - /* - * TODO: - * Return the current balance in a thread-safe way. + /** + * Returns the current balance in a thread-safe way. * - * Requirements: - * - Must be safe under concurrent reads/writes - * - Should not block unnecessarily if using read/write locks + * We acquire the lock before reading so we never observe + * a partially-written value from another thread's deposit/withdraw. */ public long getBalance() { - throw new UnsupportedOperationException("TODO: implement thread-safe balance read"); + lock.lock(); + try { + return balance; + } finally { + lock.unlock(); + } } - /* - * TODO: - * Increase balance atomically. + /** + * Adds the given amount to the balance atomically. * - * Requirements: - * - Must not lose updates under concurrency + * The lock ensures that if Thread A and Thread B both deposit + * at the same time, one waits for the other. No update is ever lost. */ public void deposit(long amount) { - throw new UnsupportedOperationException("TODO: implement thread-safe deposit"); + lock.lock(); + try { + balance += amount; + } finally { + lock.unlock(); + } } - /* - * TODO: - * Decrease balance atomically. + /** + * Subtracts the given amount from the balance atomically. * - * Requirements: - * - Must not cause race conditions - * - Negative balance handling is NOT required unless you decide - * to extend the system (optional) + * Same guarantee as deposit — no lost updates under concurrency. + * Negative balances are permitted as per the assignment spec. */ public void withdraw(long amount) { - throw new UnsupportedOperationException("TODO: implement thread-safe withdraw"); + lock.lock(); + try { + balance -= amount; + } finally { + lock.unlock(); + } } - /* - * TODO: - * Transfer money between two accounts atomically. + /** + * Transfers the given amount from THIS account to the TARGET account atomically. * - * IMPORTANT REQUIREMENTS: - * - Must be atomic (no partial transfer) - * - Must be deadlock-free - * - Must protect both source and target accounts + * --- HOW DEADLOCK IS PREVENTED --- + * A deadlock would happen if: + * Thread 1 locks Account A, then waits for Account B + * Thread 2 locks Account B, then waits for Account A + * → both wait forever. * - * HINT: - * - Consider global lock ordering using accountId - * - Or tryLock with retry strategy + * The fix: ALWAYS lock the account with the lower accountId first, + * regardless of which direction the money is flowing. + * + * Thread 1 (A→B): locks id=1 first, then id=2 ✅ + * Thread 2 (B→A): locks id=1 first, then id=2 ✅ (same order!) + * → Thread 2 simply waits until Thread 1 finishes. No deadlock. + * + * --- HOW ATOMICITY IS GUARANTEED --- + * Both locks are held before any balance changes. + * No other thread can touch either account during the transfer. + * If an exception occurs, finally blocks release both locks safely. + * Money is never lost or created. */ public void transfer(BankAccount target, long amount) { - throw new UnsupportedOperationException("TODO: implement atomic deadlock-free transfer"); + + // Determine which account to lock first (lower id = first) + BankAccount first = this.accountId < target.accountId ? this : target; + BankAccount second = this.accountId < target.accountId ? target : this; + + first.lock.lock(); + try { + second.lock.lock(); + try { + // Both accounts are now locked exclusively. + // No other thread can deposit, withdraw, or transfer + // on either account until we release both locks. + this.balance -= amount; + target.balance += amount; + } finally { + second.lock.unlock(); + } + } finally { + first.lock.unlock(); + } } -} \ No newline at end of file +} diff --git a/src/main/java/dev/banking/model/DepositTransaction.java b/src/main/java/dev/banking/model/DepositTransaction.java index f4798d2..e7250e6 100644 --- a/src/main/java/dev/banking/model/DepositTransaction.java +++ b/src/main/java/dev/banking/model/DepositTransaction.java @@ -1,36 +1,11 @@ package dev.banking.model; -/** - * Represents a deposit operation. - */ public final class DepositTransaction extends Transaction { + private final long accountId; - private final int accountId; - - public DepositTransaction( - int accountId, - int amount - ) { + public DepositTransaction(long accountId, long amount) { super(amount); - - if (accountId < 0) { - throw new IllegalArgumentException( - "Invalid account id." - ); - } - this.accountId = accountId; } - - public int getAccountId() { - return accountId; - } - - @Override - public String toString() { - return "DepositTransaction{" + - "accountId=" + accountId + - ", amount=" + getAmount() + - '}'; - } + public long getAccountId() { return accountId; } } \ No newline at end of file diff --git a/src/main/java/dev/banking/model/Transaction.java b/src/main/java/dev/banking/model/Transaction.java index 5d53921..0aa271e 100644 --- a/src/main/java/dev/banking/model/Transaction.java +++ b/src/main/java/dev/banking/model/Transaction.java @@ -1,24 +1,11 @@ package dev.banking.model; -/** - * Base class for all transaction types. - */ public abstract class Transaction { + private final long amount; - private final int amount; - - protected Transaction(int amount) { - - if (amount <= 0) { - throw new IllegalArgumentException( - "Transaction amount must be positive." - ); - } - + protected Transaction(long amount) { + if (amount <= 0) throw new IllegalArgumentException("Transaction amount must be positive."); this.amount = amount; } - - public int getAmount() { - return amount; - } + public long getAmount() { return amount; } } \ No newline at end of file diff --git a/src/main/java/dev/banking/model/TransferTransaction.java b/src/main/java/dev/banking/model/TransferTransaction.java index 89d3a57..93057b3 100644 --- a/src/main/java/dev/banking/model/TransferTransaction.java +++ b/src/main/java/dev/banking/model/TransferTransaction.java @@ -1,57 +1,15 @@ package dev.banking.model; -/** - * Represents a transfer operation between two accounts. - */ -public final class TransferTransaction - extends Transaction { +public final class TransferTransaction extends Transaction { + private final long sourceAccountId; + private final long targetAccountId; - private final int sourceAccountId; - private final int targetAccountId; - - public TransferTransaction( - int sourceAccountId, - int targetAccountId, - int amount - ) { + public TransferTransaction(long source, long target, long amount) { super(amount); - - if (sourceAccountId < 0) { - throw new IllegalArgumentException( - "Invalid source account id." - ); - } - - if (targetAccountId < 0) { - throw new IllegalArgumentException( - "Invalid target account id." - ); - } - - if (sourceAccountId == targetAccountId) { - throw new IllegalArgumentException( - "Source and target accounts must be different." - ); - } - - this.sourceAccountId = sourceAccountId; - this.targetAccountId = targetAccountId; - } - - public int getSourceAccountId() { - return sourceAccountId; - } - - public int getTargetAccountId() { - return targetAccountId; - } - - @Override - public String toString() { - return "TransferTransaction{" + - "sourceAccountId=" + sourceAccountId + - ", targetAccountId=" + targetAccountId + - ", amount=" + getAmount() + - '}'; + if (source == target) throw new IllegalArgumentException("Accounts must be different."); + this.sourceAccountId = source; + this.targetAccountId = target; } + public long getSourceAccountId() { return sourceAccountId; } + public long getTargetAccountId() { return targetAccountId; } } \ No newline at end of file diff --git a/src/main/java/dev/banking/model/WithdrawTransaction.java b/src/main/java/dev/banking/model/WithdrawTransaction.java index ba1ebd2..b8452ca 100644 --- a/src/main/java/dev/banking/model/WithdrawTransaction.java +++ b/src/main/java/dev/banking/model/WithdrawTransaction.java @@ -1,36 +1,11 @@ package dev.banking.model; -/** - * Represents a withdrawal operation. - */ public final class WithdrawTransaction extends Transaction { + private final long accountId; - private final int accountId; - - public WithdrawTransaction( - int accountId, - int amount - ) { + public WithdrawTransaction(long accountId, long amount) { super(amount); - - if (accountId < 0) { - throw new IllegalArgumentException( - "Invalid account id." - ); - } - this.accountId = accountId; } - - public int getAccountId() { - return accountId; - } - - @Override - public String toString() { - return "WithdrawTransaction{" + - "accountId=" + accountId + - ", amount=" + getAmount() + - '}'; - } + public long getAccountId() { return accountId; } } \ No newline at end of file