Last Updated on 13 August 2019   |   Print Email
In this second part of the Java synchronization tutorial series, we will show you the first solution that addresses the problem with the bank transaction example described in the first part. We need to protect the shared data which may get corrupted due to concurrent updates by multiple threads. Now, let’s see what solution Java provides to serialize access to the transfer() method of the Bank class.
1. Using Lock with ReentrantLock Object
The Java Concurrency API provides a synchronization mechanism that involves in locking/unlocking on a lock object like this:
class Clazz {
private Lock lock = new ReentrantLock();
public void method() {
lock.lock(); // 1
try {
// 2: code needs to be protected
} finally {
lock.unlock(); // 3
}
}
}
Let me explain how this mechanism works. When a thread enters line 1, it attempts to acquire the lock object and if the lock is not held by another thread, the current thread gets exclusive ownership on the lock object. If the lock is currently held by another thread, then the current thread blocks and waits until the lock is released.Once the current thread successfully acquires the lock, it executes the code in the try block without worrying about intervention of other threads. Finally the thread releases the lock and exits the method (line 3). After that, chance to acquire the lock is given to other threads. At any time, only one thread owns the lock and can execute the protected code. Other threads block and wait until the lock becomes available.The unlock statement is placed inside the finally block in order to ensure that the thread eventually relinquishes the lock in case of an exception thrown.Hence we update the Bank class as shown below:
import java.util.concurrent.locks.*;
/**
* Bank.java
* This class represents a bank that manages accounts and provides
* money transfer function.
* It demonstrates how to use the locking mechanism with a ReentrantLock object.
* @author www.codejava.net
*/
public class Bank {
public static final int MAX_ACCOUNT = 10;
public static final int MAX_AMOUNT = 10;
public static final int INITIAL_BALANCE = 100;
private Account[] accounts = new Account[MAX_ACCOUNT];
private Lock bankLock;
public Bank() {
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new Account(INITIAL_BALANCE);
}
bankLock = new ReentrantLock();
}
public void transfer(int from, int to, int amount) {
bankLock.lock();
try {
if (amount <= accounts[from].getBalance()) {
accounts[from].withdraw(amount);
accounts[to].deposit(amount);
String message = "%s transfered %d from %s to %s. Total balance: %d\n";
String threadName = Thread.currentThread().getName();
System.out.printf(message, threadName, amount, from, to, getTotalBalance());
}
} finally {
bankLock.unlock();
}
}
public int getTotalBalance() {
bankLock.lock();
try {
int total = 0;
for (int i = 0; i < accounts.length; i++) {
total += accounts[i].getBalance();
}
return total;
} finally {
bankLock.unlock();
}
}
}
Here, a ReentrantLock object is created as an instance variable of the class. The ReentrantLock class is an implementation of the Lock interface. Both are defined in the java.util.concurrent.locks package.Look closer at the transfer() method which is safeguarded for concurrent access by using a lock object as follows:
public void transfer(int from, int to, int amount) {
bankLock.lock();
try {
if (amount <= accounts[from].getBalance()) {
accounts[from].withdraw(amount);
accounts[to].deposit(amount);
String message = "%s transfered %d from %s to %s. Total balance: %d\n";
String threadName = Thread.currentThread().getName();
System.out.printf(message, threadName, amount, from, to, getTotalBalance());
}
} finally {
bankLock.unlock();
}
}
You can notice that this method calls the getTotalBalance() method which is also protected by the lock/unlock mechanism on the same bankLock object:
public int getTotalBalance() {
bankLock.lock();
try {
int total = 0;
for (int i = 0; i < accounts.length; i++) {
total += accounts[i].getBalance();
}
return total;
} finally {
bankLock.unlock();
}
}
We also need to serialize access to the getTotalBalance() method in order to avoid a situation in which other threads reading the total balance while the current thread is updating an account’s balance which affects the total balance. In other words, no thread can access the getTotalBalance() method when the current threading is executing the transfer() method because both methods are locked by the same lock object bankLock.Now, let’s recompile the Bank class and then run the TransactionTest program, you will see that the problem of corrupted total balance has gone:With the synchronization solution using lock object we have applied, the program is now running as expected: the total balance remains unchanged all the time.
2. Why ReentrantLock?
You may feel the name ReentrantLock a little bit difficult to understand, but it has a good reason for that name. The ReentrantLock allows a thread to acquire a lock it already owns multiple times recursively. Look at the transfer() method, you see that it calls the getTotalBalance() method, right? By entering the getTotalBalance() method, the current thread acquires the lock object two times, right?The number of times that a thread acquires a lock is stored in a holdcount variable. When the thread acquires the lock, the hold count is increased by 1, and when it releases the lock, hold count is decreased by 1. The lock is completely relinquished if hold count is 0. So there must be a call to unlock() for every call to lock().In the Bank class above, when the current thread acquires the lock in the transfer() method, hold count is 1; and when it acquires the lock in the getTotalBalance() method, hold count is 2. When the thread releases the lock in the getTotalBalance() method, hold count is 1. And when the thread releases the lock in the transfer() method, hold count is 0.That’s why this lock is called reentrant.
3. Locking with Condition object
In our bank example, a transaction will be processed if and only if the account has enough balance to cover the transfer:
if (amount <= accounts[from].getBalance()) {
// transfer money...
}
In case the account doesn’t have enough fund, what if we want the current thread to wait until other threads have made deposit to this account? This logic can be explained by the following pseudo-code:
If the condition (enough fund to transfer) has not been met, we can tell the current thread to wait by invoking this statement:
availableFund.await();
This causes the current thread blocks and waits, which means the current thread gives up the lock so other threads have chance to update the balance of this account. The current thread blocks until another thread calls:
availableFund.signal();
or:
availableFund.signalAll();
The difference between signal() and signalAll() is that the signal() method wakes up only one thread among the waiting ones. Which thread is chosen depends on the thread scheduler, which means there’s no guarantee that the current thread will wake up if one thread invokes signal().And the signalAll() method wakes up all threads which are currently waiting. Note that it’s up to the thread scheduler decides which thread takes the turn. All threads awake but only one is granted access to the lock. That also means there’s no guarantee that the current thread can acquire the lock again though it is waken up. If it is the case, the thread continues blocking and waiting for other chances, until it gets the locks and exits the method.Now, let’s update the Bank class as follows:
import java.util.concurrent.locks.*;
/**
* Bank.java
* This class represents a bank that manages accounts and provides
* money transfer function.
* It demonstrates how to use the locking mechanism with a condition object.
* @author www.codejava.net
*/
public class Bank {
public static final int MAX_ACCOUNT = 10;
public static final int MAX_AMOUNT = 10;
public static final int INITIAL_BALANCE = 100;
private Account[] accounts = new Account[MAX_ACCOUNT];
private Lock bankLock;
private Condition availableFund;
public Bank() {
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new Account(INITIAL_BALANCE);
}
bankLock = new ReentrantLock();
availableFund = bankLock.newCondition();
}
public void transfer(int from, int to, int amount) {
bankLock.lock();
try {
while (accounts[from].getBalance() < amount) {
availableFund.await();
}
accounts[from].withdraw(amount);
accounts[to].deposit(amount);
String message = "%s transfered %d from %s to %s. Total balance: %d\n";
String threadName = Thread.currentThread().getName();
System.out.printf(message, threadName, amount, from, to, getTotalBalance());
availableFund.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bankLock.unlock();
}
}
public int getTotalBalance() {
bankLock.lock();
try {
int total = 0;
for (int i = 0; i < accounts.length; i++) {
total += accounts[i].getBalance();
}
return total;
} finally {
bankLock.unlock();
}
}
}
Recompile this class and then run the TransactionTest program again and observe the result yourself. So using Condition object would be useful in case you want the current thread to wait until the condition is met, rather than giving up immediately if it is not.The technique we have experienced so far is called explicit locking mechanism, which uses a concrete Lock object with a Condition object.In the next part, you will learn about the implicit locking mechanism using the synchronized keyword. Part 3: Java Synchronization Tutorial Part 3 - Using synchronized keyword (Intrinsic locking)
Nam Ha Minh is certified Java programmer (SCJP and SCWCD). He began programming with Java back in the days of Java 1.4 and has been passionate about it ever since. You can connect with him on Facebook and watch his Java videos on YouTube.
Comments