From 43a2d4b0025d4fda67564a3c6ece3c89ffaca7b6 Mon Sep 17 00:00:00 2001 From: Kah Goh Date: Tue, 7 Oct 2025 21:02:29 +0800 Subject: [PATCH 1/6] Add approach for Bank Account Resolves #2711 --- .../bank-account/.approaches/config.json | 45 +++ .../bank-account/.approaches/introduction.md | 351 ++++++++++++++++++ .../.approaches/readwrite-lock/content.md | 108 ++++++ .../.approaches/readwrite-lock/snippet.txt | 7 + .../.approaches/reentrant-lock/content.md | 96 +++++ .../.approaches/reentrant-lock/snippet.txt | 7 + .../synchronized-methods/content.md | 75 ++++ .../synchronized-methods/snippet.txt | 3 + .../synchronized-statements/content.md | 123 ++++++ .../synchronized-statements/snippet.txt | 7 + 10 files changed, 822 insertions(+) create mode 100644 exercises/practice/bank-account/.approaches/config.json create mode 100644 exercises/practice/bank-account/.approaches/introduction.md create mode 100644 exercises/practice/bank-account/.approaches/readwrite-lock/content.md create mode 100644 exercises/practice/bank-account/.approaches/readwrite-lock/snippet.txt create mode 100644 exercises/practice/bank-account/.approaches/reentrant-lock/content.md create mode 100644 exercises/practice/bank-account/.approaches/reentrant-lock/snippet.txt create mode 100644 exercises/practice/bank-account/.approaches/synchronized-methods/content.md create mode 100644 exercises/practice/bank-account/.approaches/synchronized-methods/snippet.txt create mode 100644 exercises/practice/bank-account/.approaches/synchronized-statements/content.md create mode 100644 exercises/practice/bank-account/.approaches/synchronized-statements/snippet.txt diff --git a/exercises/practice/bank-account/.approaches/config.json b/exercises/practice/bank-account/.approaches/config.json new file mode 100644 index 000000000..b91f6f034 --- /dev/null +++ b/exercises/practice/bank-account/.approaches/config.json @@ -0,0 +1,45 @@ +{ + "introduction": { + "authors": [ + "kahgoh" + ] + }, + "approaches": [ + { + "uuid": "78d753b0-aa58-43dc-83c0-9ea41496de84", + "slug": "synchronized-methods", + "title": "Synchronized methods", + "blurb": "Use synchronized methods to prevent methods from running simultaneously in different threads", + "authors": [ + "kahgoh" + ] + }, + { + "uuid": "5f3b0152-02eb-40d5-9104-5edc30b4447e", + "slug": "synchronized-statements", + "title": "Synchronized statements", + "blurb": "Use an object to prevent threads from running blocks of code at the same time", + "authors": [ + "kahgoh" + ] + }, + { + "uuid": "0acd6f2b-27d0-4ae6-9c22-22b0b2047039", + "slug": "reentrant-lock", + "title": "Reentrant lock", + "blurb": "Use ", + "authors": [ + "kahgoh" + ] + }, + { + "uuid": "4ad42f88-f750-4af9-bbbd-d8b2dc5e8078", + "slug": "readwrite-lock", + "title": "Readwrite lock", + "blurb": "Use separate read and write locks to achieve greater concurrency", + "authors": [ + "kahgoh" + ] + } + ] +} diff --git a/exercises/practice/bank-account/.approaches/introduction.md b/exercises/practice/bank-account/.approaches/introduction.md new file mode 100644 index 000000000..c4e84422f --- /dev/null +++ b/exercises/practice/bank-account/.approaches/introduction.md @@ -0,0 +1,351 @@ +# Introduction + +In Bank Account, you are tasked with implementing a number of operations that can be performed on a bank account. +However, these operations may be performed by multiple threads at the same time. + +## General guidance + +The key to solving Bank Account is to prevent an account from being updated from multiple threads at the same time. +For example, consider an account that begins with $0. +A $10 deposit is made twice. +Each transaction starts a thread. +If the threads happen to start simultaneously, they might both see the account starting $0. +They each add $10 to this amount and update the account accordingly. +In this case, each thread sets the amount to $10 even though the transaction was made twice. + +The problem here is both threads saw that was $0 in the account prior to adding the deposit. +Instead, each thread needs to take turn to process the deposit for the account so that they can process the transaction one at a time. +This way, the later thread can "see" the deposited amount from the first thread. + +## Approach: Synchronized methods + +```java +class BankAccount { + private int balance = 0; + private boolean isClosed = true; + + synchronized void open() throws BankAccountActionInvalidException { + if (!isClosed) { + throw new BankAccountActionInvalidException("Account already open"); + } + isClosed = false; + balance = 0; + } + + synchronized void close() throws BankAccountActionInvalidException { + if (isClosed) { + throw new BankAccountActionInvalidException("Account not open"); + } + isClosed = true; + } + + synchronized int getBalance() throws BankAccountActionInvalidException { + checkIfClosed(); + return balance; + } + + synchronized void deposit(int amount) throws BankAccountActionInvalidException { + checkIfClosed(); + checkIfValidAmount(amount); + + balance += amount; + } + + synchronized void withdraw(int amount) throws BankAccountActionInvalidException { + checkIfClosed(); + checkIfValidAmount(amount); + checkIfEnoughMoneyInAccount(amount); + + balance -= amount; + } + + private void checkIfValidAmount(int amount) throws BankAccountActionInvalidException { + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + } + + private void checkIfEnoughMoneyInAccount(int amount) throws BankAccountActionInvalidException { + if (balance == 0) { + throw new BankAccountActionInvalidException("Cannot withdraw money from an empty account"); + } + if (balance - amount < 0) { + throw new BankAccountActionInvalidException("Cannot withdraw more money than is currently in the account"); + } + } + + private void checkIfClosed() throws BankAccountActionInvalidException { + if (isClosed) { + throw new BankAccountActionInvalidException("Account closed"); + } + } +} +``` + +For more information, check the [Synchronized methods approach][approach-synchronized-methods]. + +## Approach: Synchronized statements + +```java +class BankAccount { + private final Object lock = new Object(); + private int balance = 0; + private boolean isClosed = true; + + void open() throws BankAccountActionInvalidException { + synchronized(lock) { + if (!isClosed) { + throw new BankAccountActionInvalidException("Account already open"); + } + isClosed = false; + balance = 0; + } + } + + void close() throws BankAccountActionInvalidException { + synchronized(lock) { + if (isClosed) { + throw new BankAccountActionInvalidException("Account not open"); + } + isClosed = true; + } + } + + int getBalance() throws BankAccountActionInvalidException { + synchronized(lock) { + checkIfClosed(); + return balance; + } + } + + void deposit(int amount) throws BankAccountActionInvalidException { + synchronized(lock) { + checkIfClosed(); + checkIfValidAmount(amount); + + balance += amount; + } + } + + void withdraw(int amount) throws BankAccountActionInvalidException { + synchronized(lock) { + checkIfClosed(); + checkIfValidAmount(amount); + checkIfEnoughMoneyInAccount(amount); + + balance -= amount; + } + } + + private void checkIfValidAmount(int amount) throws BankAccountActionInvalidException { + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + } + + private void checkIfEnoughMoneyInAccount(int amount) throws BankAccountActionInvalidException { + if (balance == 0) { + throw new BankAccountActionInvalidException("Cannot withdraw money from an empty account"); + } + if (balance - amount < 0) { + throw new BankAccountActionInvalidException("Cannot withdraw more money than is currently in the account"); + } + } + + private void checkIfClosed() throws BankAccountActionInvalidException { + if (isClosed) { + throw new BankAccountActionInvalidException("Account closed"); + } + } +} +``` + +For more information, check the [Synchronized statements approach][approach-synchronized-statements]. + +## Approach: Reentrant lock + +```java +import java.util.concurrent.locks.ReentrantLock; + +class BankAccount { + + private final ReentrantLock lock = new ReentrantLock(); + + private boolean isOpen = false; + private int balance = 0; + + void open() throws BankAccountActionInvalidException { + lock.lock(); + try { + if (isOpen) { + throw new BankAccountActionInvalidException("Account already open"); + } + isOpen = true; + balance = 0; + } finally { + lock.unlock(); + } + } + + void close() throws BankAccountActionInvalidException { + lock.lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account not open"); + } + isOpen = false; + } finally { + lock.unlock(); + } + } + + int getBalance() throws BankAccountActionInvalidException { + lock.lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + return balance; + } finally { + lock.unlock(); + } + } + + void deposit(int amount) throws BankAccountActionInvalidException { + lock.lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + balance += amount; + } finally { + lock.unlock(); + } + } + + void withdraw(int amount) throws BankAccountActionInvalidException { + lock.lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + if (amount > balance) { + throw new BankAccountActionInvalidException("Cannot withdraw more money than is currently in the account"); + } + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + balance -= amount; + } finally { + lock.unlock(); + } + } + +} +``` + +For more information, check the [Reentrant lock approach][approach-reentrant-lock]. + +## Approach: Read write lock + +```java +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +class BankAccount { + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private boolean isOpen = false; + + private int balance = 0; + + void open() throws BankAccountActionInvalidException { + lock.writeLock().lock(); + try { + if (isOpen) { + throw new BankAccountActionInvalidException("Account already open"); + } + isOpen = true; + balance = 0; + } finally { + lock.writeLock().unlock(); + } + } + + void close() throws BankAccountActionInvalidException { + lock.writeLock().lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account not open"); + } + isOpen = false; + } finally { + lock.writeLock().unlock(); + } + } + + int getBalance() throws BankAccountActionInvalidException { + lock.readLock().lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + return balance; + } finally { + lock.readLock().unlock(); + } + } + + void deposit(int amount) throws BankAccountActionInvalidException { + lock.writeLock().lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + balance += amount; + } finally { + lock.writeLock().unlock(); + } + } + + void withdraw(int amount) throws BankAccountActionInvalidException { + lock.writeLock().lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + if (amount > balance) { + throw new BankAccountActionInvalidException("Cannot withdraw more money than is currently in the account"); + } + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + balance -= amount; + } finally { + lock.writeLock().unlock(); + } + } +} +``` + +For more information, check the [Read write lock approach][approach-read-write-lock]. + +## Which approach to use? + +- The synchronized methods is the simplest, requiring no extra objects to be created. +- Synchronized statements provide greater control over which code statements are performed with a lock and which object is to be used as the lock. +- The read write lock allows greater concurrency by letting multiple read operations, such as `getBalance`, to run in parallel. + However, it requires the lock to be explicitly released. + +[approach-read-write-lock]: https://exercism.org/tracks/java/exercises/bank-account/approaches/read-write-lock +[approach-reentrant-lock]: https://exercism.org/tracks/java/exercises/bank-acconuunt/approaches/reentrant-lock +[approach-synchronized-methods]: https://exercism.org/tracks/java/exercises/bank-account/approaches/synchronized-methods +[approach-synchronized-statements]: https://exercism.org/tracks/java/exercises/bank-account/approaches/synchronzied-statements diff --git a/exercises/practice/bank-account/.approaches/readwrite-lock/content.md b/exercises/practice/bank-account/.approaches/readwrite-lock/content.md new file mode 100644 index 000000000..4778f0be1 --- /dev/null +++ b/exercises/practice/bank-account/.approaches/readwrite-lock/content.md @@ -0,0 +1,108 @@ +# Readwrite Lock + +```java +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +class BankAccount { + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private boolean isOpen = false; + + private int balance = 0; + + void open() throws BankAccountActionInvalidException { + lock.writeLock().lock(); + try { + if (isOpen) { + throw new BankAccountActionInvalidException("Account already open"); + } + isOpen = true; + balance = 0; + } finally { + lock.writeLock().unlock(); + } + } + + void close() throws BankAccountActionInvalidException { + lock.writeLock().lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account not open"); + } + isOpen = false; + } finally { + lock.writeLock().unlock(); + } + } + + int getBalance() throws BankAccountActionInvalidException { + lock.readLock().lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + return balance; + } finally { + lock.readLock().unlock(); + } + } + + void deposit(int amount) throws BankAccountActionInvalidException { + lock.writeLock().lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + balance += amount; + } finally { + lock.writeLock().unlock(); + } + } + + void withdraw(int amount) throws BankAccountActionInvalidException { + lock.writeLock().lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + if (amount > balance) { + throw new BankAccountActionInvalidException("Cannot withdraw more money than is currently in the account"); + } + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + balance -= amount; + } finally { + lock.writeLock().unlock(); + } + } +} +``` + +A [ReadWriteLock][docs-readwritelock] provides two types of locks - one for reading the other for writing. +[ReentrantReadWriteLock][docs-reentrantreadwritelock] is an implementation of a [ReadWriteLock][docs-readwritelock]. + +Read locks are intended for read only type operations, such as `getBalance`, and is acquired by calling `readLock().lock()` on the [ReadWriteLock][docs-readwritelock]. +Multiple threads are allowed to acquire read locks at the same time because they are expected to only read data. +This means multiple threads can run `getBalance` at the same time. + +Write locks are for write operations, such as `withdraw` and `deposit`. +It is also used in `open` and `close` as they change the state of the `BankAccount` (they "write" to the `isOpen` field). +Write locks are acquired by calling `writeLock().lock()` on the [ReadWriteLock][docs-readwritelock]. + +Only one thread can hold a write lock at a time. +Therefore, `withdraw`, `deposit`, `open` and `close` can not run at the same time. +Additionally, a thread must _also_ wait for _all_ read locks to be released to obtain a write lock. +Similarly, threads must wait for write locks to be released before they granted a read lock. +This means `getBalance` also can not run at the same time as any of the `withdraw`, `deposit`, `open` and `close` methods. + +The locks are released in the `finally` block to ensure they are released, even when an exception is thrown. + +[docs-readwritelock]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/locks/ReadWriteLock.html +[docs-reentrantreadwritelock]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/locks/ReentrantReadWriteLock.html diff --git a/exercises/practice/bank-account/.approaches/readwrite-lock/snippet.txt b/exercises/practice/bank-account/.approaches/readwrite-lock/snippet.txt new file mode 100644 index 000000000..45fcdf98d --- /dev/null +++ b/exercises/practice/bank-account/.approaches/readwrite-lock/snippet.txt @@ -0,0 +1,7 @@ +private final ReadWriteLock lock = new ReentrantReadWriteLock(); +lock.readLock().lock(); +try { + balance += amount; +} finally { + lock.readLock().unlock(); +} \ No newline at end of file diff --git a/exercises/practice/bank-account/.approaches/reentrant-lock/content.md b/exercises/practice/bank-account/.approaches/reentrant-lock/content.md new file mode 100644 index 000000000..47e6f6a23 --- /dev/null +++ b/exercises/practice/bank-account/.approaches/reentrant-lock/content.md @@ -0,0 +1,96 @@ +# Reentrant Lock + +```java +import java.util.concurrent.locks.ReentrantLock; + +class BankAccount { + + private final ReentrantLock lock = new ReentrantLock(); + + private boolean isOpen = false; + private int balance = 0; + + void open() throws BankAccountActionInvalidException { + lock.lock(); + try { + if (isOpen) { + throw new BankAccountActionInvalidException("Account already open"); + } + isOpen = true; + balance = 0; + } finally { + lock.unlock(); + } + } + + void close() throws BankAccountActionInvalidException { + lock.lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account not open"); + } + isOpen = false; + } finally { + lock.unlock(); + } + } + + int getBalance() throws BankAccountActionInvalidException { + lock.lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + return balance; + } finally { + lock.unlock(); + } + } + + void deposit(int amount) throws BankAccountActionInvalidException { + lock.lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + balance += amount; + } finally { + lock.unlock(); + } + } + + void withdraw(int amount) throws BankAccountActionInvalidException { + lock.lock(); + try { + if (!isOpen) { + throw new BankAccountActionInvalidException("Account closed"); + } + if (amount > balance) { + throw new BankAccountActionInvalidException("Cannot withdraw more money than is currently in the account"); + } + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + balance -= amount; + } finally { + lock.unlock(); + } + } + +} +``` + +A [ReentrantLock][docs-reentrantlock] object represents a lock that threads must acquire to perform certain operations. +It used here by the operation methods to ensure they are not trying to update the bank account at the same time. + +The lock is requested by calling [lock][docs-reentrantlock-lock]. +The lock is released at the end of the operation by calling [unlock][docs-reentrantlock-unlock] in a `finally` block. +This is important to ensure that the lock is released when it is no longer needed, especially if an exception is thrown. +The re-entrant nature of the lock means a thread will be granted a lock again if it already has the lock. + +[docs-reentrantlock]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html +[docs-reentrantlock-lock]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html#lock() +[docs-reentrantlock-unlock]: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html#unlock() diff --git a/exercises/practice/bank-account/.approaches/reentrant-lock/snippet.txt b/exercises/practice/bank-account/.approaches/reentrant-lock/snippet.txt new file mode 100644 index 000000000..2014610f8 --- /dev/null +++ b/exercises/practice/bank-account/.approaches/reentrant-lock/snippet.txt @@ -0,0 +1,7 @@ +private final ReentrantLock lock = new ReentrantLock(); +lock.lock(); +try { + balance += amount; +} finally { + lock.unlock(); +} \ No newline at end of file diff --git a/exercises/practice/bank-account/.approaches/synchronized-methods/content.md b/exercises/practice/bank-account/.approaches/synchronized-methods/content.md new file mode 100644 index 000000000..005a39b25 --- /dev/null +++ b/exercises/practice/bank-account/.approaches/synchronized-methods/content.md @@ -0,0 +1,75 @@ +# Synchronized methods + +```java +class BankAccount { + private int balance = 0; + private boolean isClosed = true; + + synchronized void open() throws BankAccountActionInvalidException { + if (!isClosed) { + throw new BankAccountActionInvalidException("Account already open"); + } + isClosed = false; + balance = 0; + } + + synchronized void close() throws BankAccountActionInvalidException { + if (isClosed) { + throw new BankAccountActionInvalidException("Account not open"); + } + isClosed = true; + } + + synchronized int getBalance() throws BankAccountActionInvalidException { + checkIfClosed(); + return balance; + } + + synchronized void deposit(int amount) throws BankAccountActionInvalidException { + checkIfClosed(); + checkIfValidAmount(amount); + + balance += amount; + } + + synchronized void withdraw(int amount) throws BankAccountActionInvalidException { + checkIfClosed(); + checkIfValidAmount(amount); + checkIfEnoughMoneyInAccount(amount); + + balance -= amount; + } + + private void checkIfValidAmount(int amount) throws BankAccountActionInvalidException { + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + } + + private void checkIfEnoughMoneyInAccount(int amount) throws BankAccountActionInvalidException { + if (balance == 0) { + throw new BankAccountActionInvalidException("Cannot withdraw money from an empty account"); + } + if (balance - amount < 0) { + throw new BankAccountActionInvalidException("Cannot withdraw more money than is currently in the account"); + } + } + + private void checkIfClosed() throws BankAccountActionInvalidException { + if (isClosed) { + throw new BankAccountActionInvalidException("Account closed"); + } + } +} +``` + +Each operation method is marked `synchronized`. +This tells the thread to acquire a lock on the `BankAccount` object _before_ executing the method. +If any other thread holds a lock on the `BankAccount` object, it must wait for the other thread to release the lock. + +~~~~exercism/note +In Java, the is one other way to acquire a lock on the `BankAccount` objct - synchronized objects. +Since synchronized methods uses a lock on the `BankAccount` object, it will have to wait for locks on the `BankAccount` acquired as synchronized object to be released. +~~~~ + +The lock is automatically released when the method finishes. diff --git a/exercises/practice/bank-account/.approaches/synchronized-methods/snippet.txt b/exercises/practice/bank-account/.approaches/synchronized-methods/snippet.txt new file mode 100644 index 000000000..61ca7d4ec --- /dev/null +++ b/exercises/practice/bank-account/.approaches/synchronized-methods/snippet.txt @@ -0,0 +1,3 @@ +synchronized void deposit(int amount) throws BankAccountActionInvalidException { + balance += amount; +} \ No newline at end of file diff --git a/exercises/practice/bank-account/.approaches/synchronized-statements/content.md b/exercises/practice/bank-account/.approaches/synchronized-statements/content.md new file mode 100644 index 000000000..ecab25dda --- /dev/null +++ b/exercises/practice/bank-account/.approaches/synchronized-statements/content.md @@ -0,0 +1,123 @@ +# Synchronized statements + +```java +class BankAccount { + private final Object lock = new Object(); + private int balance = 0; + private boolean isClosed = true; + + void open() throws BankAccountActionInvalidException { + synchronized(lock) { + if (!isClosed) { + throw new BankAccountActionInvalidException("Account already open"); + } + isClosed = false; + balance = 0; + } + } + + void close() throws BankAccountActionInvalidException { + synchronized(lock) { + if (isClosed) { + throw new BankAccountActionInvalidException("Account not open"); + } + isClosed = true; + } + } + + int getBalance() throws BankAccountActionInvalidException { + synchronized(lock) { + checkIfClosed(); + return balance; + } + } + + void deposit(int amount) throws BankAccountActionInvalidException { + synchronized(lock) { + checkIfClosed(); + checkIfValidAmount(amount); + + balance += amount; + } + } + + void withdraw(int amount) throws BankAccountActionInvalidException { + synchronized(lock) { + checkIfClosed(); + checkIfValidAmount(amount); + checkIfEnoughMoneyInAccount(amount); + + balance -= amount; + } + } + + private void checkIfValidAmount(int amount) throws BankAccountActionInvalidException { + if (amount < 0) { + throw new BankAccountActionInvalidException("Cannot deposit or withdraw negative amount"); + } + } + + private void checkIfEnoughMoneyInAccount(int amount) throws BankAccountActionInvalidException { + if (balance == 0) { + throw new BankAccountActionInvalidException("Cannot withdraw money from an empty account"); + } + if (balance - amount < 0) { + throw new BankAccountActionInvalidException("Cannot withdraw more money than is currently in the account"); + } + } + + private void checkIfClosed() throws BankAccountActionInvalidException { + if (isClosed) { + throw new BankAccountActionInvalidException("Account closed"); + } + } +} +``` + +In this approach, the operation methods, such as `open`, `close`, `deposit` and `withdraw`, perform their operations in a `synchronized` code block. +A lock is acquired on the synchronized object (`lock`) before the statements inside the block are executed. +If another thread has a lock on the object, it must wait for it to be released. +The lock is released after the block is executed. + +## Using `this` as the synchronized object + +Any object can be used as the lock, including `this`. +For example: + +```java +int getBalance() throws BankAccountActionInvalidException { + synchronized(this) { + checkIfClosed(); + return balance; + } +} +``` + +This is the same as using a synchronized method. +For example: + +```java +synchronized int getBalance() throws BankAccountActionInvalidException { + checkIfClosed(); + return balance; +} +``` + +When using [synchronized methods][approach-synchronized-methods] and `synchronized(this)`, it is important to keep in mind that may be trying to acquire a lock on the same instance. +For example: + +```java +BankAccount account = new BankAccount(); + +Thread thread1 = new Thread(() -> { + account.withdraw(5); +}); + +Thread thread2 = new Thread(() -> { + synchronized (account) { + // Code in here can not run at same time as account.withdraw in thread1. + } +}); +``` + +[approach-synchronized-methods]: https://exercism.org/tracks/java/exercises/bank-account/approaches/synchronized-methods diff --git a/exercises/practice/bank-account/.approaches/synchronized-statements/snippet.txt b/exercises/practice/bank-account/.approaches/synchronized-statements/snippet.txt new file mode 100644 index 000000000..6b3a368fb --- /dev/null +++ b/exercises/practice/bank-account/.approaches/synchronized-statements/snippet.txt @@ -0,0 +1,7 @@ +private final Object lock = new Object(); + +void deposit(int amount) throws BankAccountActionInvalidException { + synchronized(lock) { + balance += amount; + } +} \ No newline at end of file From 5e603c1cf9877d4f868e34ba563ddbcf403f0569 Mon Sep 17 00:00:00 2001 From: Kah Goh Date: Wed, 8 Oct 2025 16:50:42 +0800 Subject: [PATCH 2/6] Fix wording and spelling Co-authored-by: Jagdish Prajapati --- .../practice/bank-account/.approaches/introduction.md | 8 ++++---- .../bank-account/.approaches/readwrite-lock/content.md | 6 +++--- .../bank-account/.approaches/reentrant-lock/content.md | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/exercises/practice/bank-account/.approaches/introduction.md b/exercises/practice/bank-account/.approaches/introduction.md index c4e84422f..ac8d3c506 100644 --- a/exercises/practice/bank-account/.approaches/introduction.md +++ b/exercises/practice/bank-account/.approaches/introduction.md @@ -13,8 +13,8 @@ If the threads happen to start simultaneously, they might both see the account s They each add $10 to this amount and update the account accordingly. In this case, each thread sets the amount to $10 even though the transaction was made twice. -The problem here is both threads saw that was $0 in the account prior to adding the deposit. -Instead, each thread needs to take turn to process the deposit for the account so that they can process the transaction one at a time. +The problem here is that both threads saw that there was $0 in the account prior to adding the deposit. +Instead, each thread needs to take turns to process the deposit for the account so that they can process the transaction one at a time. This way, the later thread can "see" the deposited amount from the first thread. ## Approach: Synchronized methods @@ -342,10 +342,10 @@ For more information, check the [Read write lock approach][approach-read-write-l - The synchronized methods is the simplest, requiring no extra objects to be created. - Synchronized statements provide greater control over which code statements are performed with a lock and which object is to be used as the lock. -- The read write lock allows greater concurrency by letting multiple read operations, such as `getBalance`, to run in parallel. +- The read write lock allows greater concurrency by letting multiple read operations, such as `getBalance`, run in parallel. However, it requires the lock to be explicitly released. -[approach-read-write-lock]: https://exercism.org/tracks/java/exercises/bank-account/approaches/read-write-lock +[approach-read-write-lock]: https://exercism.org/tracks/java/exercises/bank-account/approaches/readwrite-lock [approach-reentrant-lock]: https://exercism.org/tracks/java/exercises/bank-acconuunt/approaches/reentrant-lock [approach-synchronized-methods]: https://exercism.org/tracks/java/exercises/bank-account/approaches/synchronized-methods [approach-synchronized-statements]: https://exercism.org/tracks/java/exercises/bank-account/approaches/synchronzied-statements diff --git a/exercises/practice/bank-account/.approaches/readwrite-lock/content.md b/exercises/practice/bank-account/.approaches/readwrite-lock/content.md index 4778f0be1..5db33cc0a 100644 --- a/exercises/practice/bank-account/.approaches/readwrite-lock/content.md +++ b/exercises/practice/bank-account/.approaches/readwrite-lock/content.md @@ -85,10 +85,10 @@ class BankAccount { } ``` -A [ReadWriteLock][docs-readwritelock] provides two types of locks - one for reading the other for writing. +A [ReadWriteLock][docs-readwritelock] provides two types of locks - one for reading, the other for writing. [ReentrantReadWriteLock][docs-reentrantreadwritelock] is an implementation of a [ReadWriteLock][docs-readwritelock]. -Read locks are intended for read only type operations, such as `getBalance`, and is acquired by calling `readLock().lock()` on the [ReadWriteLock][docs-readwritelock]. +Read locks are intended for read-only type operations, such as `getBalance`, and are acquired by calling `readLock().lock()` on the [ReadWriteLock][docs-readwritelock]. Multiple threads are allowed to acquire read locks at the same time because they are expected to only read data. This means multiple threads can run `getBalance` at the same time. @@ -99,7 +99,7 @@ Write locks are acquired by calling `writeLock().lock()` on the [ReadWriteLock][ Only one thread can hold a write lock at a time. Therefore, `withdraw`, `deposit`, `open` and `close` can not run at the same time. Additionally, a thread must _also_ wait for _all_ read locks to be released to obtain a write lock. -Similarly, threads must wait for write locks to be released before they granted a read lock. +Similarly, threads must wait for write locks to be released before they are granted a read lock. This means `getBalance` also can not run at the same time as any of the `withdraw`, `deposit`, `open` and `close` methods. The locks are released in the `finally` block to ensure they are released, even when an exception is thrown. diff --git a/exercises/practice/bank-account/.approaches/reentrant-lock/content.md b/exercises/practice/bank-account/.approaches/reentrant-lock/content.md index 47e6f6a23..efa9f9512 100644 --- a/exercises/practice/bank-account/.approaches/reentrant-lock/content.md +++ b/exercises/practice/bank-account/.approaches/reentrant-lock/content.md @@ -84,7 +84,7 @@ class BankAccount { ``` A [ReentrantLock][docs-reentrantlock] object represents a lock that threads must acquire to perform certain operations. -It used here by the operation methods to ensure they are not trying to update the bank account at the same time. +It is used here by the operation methods to ensure they are not trying to update the bank account at the same time. The lock is requested by calling [lock][docs-reentrantlock-lock]. The lock is released at the end of the operation by calling [unlock][docs-reentrantlock-unlock] in a `finally` block. From 47100fe324014cdafd088371bda77af469273843 Mon Sep 17 00:00:00 2001 From: Kah Goh Date: Wed, 8 Oct 2025 17:00:50 +0800 Subject: [PATCH 3/6] Replace "syncrhonized objects" with "synchronized statements" This is to be more consistent with the terminology used in the "synchronized statements" approach. --- .../.approaches/synchronized-methods/content.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/exercises/practice/bank-account/.approaches/synchronized-methods/content.md b/exercises/practice/bank-account/.approaches/synchronized-methods/content.md index 005a39b25..509d44f86 100644 --- a/exercises/practice/bank-account/.approaches/synchronized-methods/content.md +++ b/exercises/practice/bank-account/.approaches/synchronized-methods/content.md @@ -68,8 +68,10 @@ This tells the thread to acquire a lock on the `BankAccount` object _before_ exe If any other thread holds a lock on the `BankAccount` object, it must wait for the other thread to release the lock. ~~~~exercism/note -In Java, the is one other way to acquire a lock on the `BankAccount` objct - synchronized objects. -Since synchronized methods uses a lock on the `BankAccount` object, it will have to wait for locks on the `BankAccount` acquired as synchronized object to be released. +In Java, the is one other way to acquire a lock on the `BankAccount` object - [synchronized statements][approach-synchronized-statements]. +Since synchronized methods uses a lock on the `BankAccount` object, it will also have to wait for locks on the `BankAccount` that are used by [synchronized statements][approach-synchronized-statements] to be reused. + +[approach-synchronized-statements]: https://exercism.org/tracks/java/exercises/bank-account/approaches/synchronzied-statements ~~~~ The lock is automatically released when the method finishes. From 90247d6f892ec01c8406a0d8b0cab1d631160625 Mon Sep 17 00:00:00 2001 From: Kah Goh Date: Wed, 8 Oct 2025 17:07:15 +0800 Subject: [PATCH 4/6] Add explanation for lock in synchronized method option --- .../bank-account/.approaches/synchronized-statements/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/bank-account/.approaches/synchronized-statements/content.md b/exercises/practice/bank-account/.approaches/synchronized-statements/content.md index ecab25dda..beceabc6e 100644 --- a/exercises/practice/bank-account/.approaches/synchronized-statements/content.md +++ b/exercises/practice/bank-account/.approaches/synchronized-statements/content.md @@ -93,7 +93,7 @@ int getBalance() throws BankAccountActionInvalidException { } ``` -This is the same as using a synchronized method. +This is the same as using a [synchronized method][approach-synchronized-methods], which requires a lock on the same `this` object to run the method. For example: ```java From 05c02457a67c937f611bfd639d454eaf6d69f203 Mon Sep 17 00:00:00 2001 From: Kah Goh Date: Thu, 9 Oct 2025 17:25:14 +0800 Subject: [PATCH 5/6] Add missing word Co-authored-by: Jagdish Prajapati --- .../bank-account/.approaches/synchronized-statements/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/bank-account/.approaches/synchronized-statements/content.md b/exercises/practice/bank-account/.approaches/synchronized-statements/content.md index beceabc6e..8d17806b1 100644 --- a/exercises/practice/bank-account/.approaches/synchronized-statements/content.md +++ b/exercises/practice/bank-account/.approaches/synchronized-statements/content.md @@ -103,7 +103,7 @@ synchronized int getBalance() throws BankAccountActionInvalidException { } ``` -When using [synchronized methods][approach-synchronized-methods] and `synchronized(this)`, it is important to keep in mind that may be trying to acquire a lock on the same instance. +When using [synchronized methods][approach-synchronized-methods] and `synchronized(this)`, it is important to keep in mind that it may be trying to acquire a lock on the same instance. For example: ```java From 85c3b7b3b7e0ecb08072b42241171e67065e470f Mon Sep 17 00:00:00 2001 From: Kah Goh Date: Thu, 9 Oct 2025 21:29:09 +0800 Subject: [PATCH 6/6] Fix grammar Co-authored-by: Jagdish Prajapati --- .../bank-account/.approaches/synchronized-methods/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/bank-account/.approaches/synchronized-methods/content.md b/exercises/practice/bank-account/.approaches/synchronized-methods/content.md index 509d44f86..7c1730694 100644 --- a/exercises/practice/bank-account/.approaches/synchronized-methods/content.md +++ b/exercises/practice/bank-account/.approaches/synchronized-methods/content.md @@ -69,7 +69,7 @@ If any other thread holds a lock on the `BankAccount` object, it must wait for t ~~~~exercism/note In Java, the is one other way to acquire a lock on the `BankAccount` object - [synchronized statements][approach-synchronized-statements]. -Since synchronized methods uses a lock on the `BankAccount` object, it will also have to wait for locks on the `BankAccount` that are used by [synchronized statements][approach-synchronized-statements] to be reused. +Since synchronized methods use a lock on the `BankAccount` object, it will also have to wait for locks on the `BankAccount` that are used by [synchronized statements][approach-synchronized-statements] to be reused. [approach-synchronized-statements]: https://exercism.org/tracks/java/exercises/bank-account/approaches/synchronzied-statements ~~~~