Visit the bank-account exercise on Exercism to read the full instructions and download the exercise files.
Dig Deeper
Synchronized methods
Synchronized methods
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.
In Java, the is one other way to acquire a lock on the `BankAccount` object - [synchronized statements][approach-synchronized-statements].
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
The lock is automatically released when the method finishes.
Synchronized statements
Synchronized statements
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:
int getBalance() throws BankAccountActionInvalidException {
synchronized(this) {
checkIfClosed();
return balance;
}
}
This is the same as using a synchronized method, which requires a lock on the same this object to run the method.
For example:
synchronized int getBalance() throws BankAccountActionInvalidException {
checkIfClosed();
return balance;
}
When using 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:
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.
}
});
Reentrant lock
Reentrant Lock
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 object represents a lock that threads must acquire to perform certain operations.
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.
The lock is released at the end of the operation by calling 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.
Source: Exercism java/bank-account