Let's create Bank class that has an array of accounts and can transfer money from one account to the other. Of course, it can be done in multiple concurrent threads and thus wee need synchronization mechanism.
Consider following implementation of the Bank class using locks and conditions:
package locks;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Bank {
Lock bankLock;
Condition sufficientFunds;
double[] accounts;
public Bank(int accounts, double initialBalance) {
this.accounts = new double[accounts];
Arrays.fill(this.accounts, initialBalance);
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int to, int from, double amount) {
bankLock.lock();
try {
System.out.printf(
"Checking funds [%.2f] on account [%d]: [%.2f]\n",
amount, from, accounts[from]);
int count = 0;
while (accounts[from] < amount) {
sufficientFunds.await(5, TimeUnit.SECONDS);
count++;
if (count > 3) {
System.out.println("--- could not transfer money ---");
return;
}
}
System.out.printf(
"Transferring [%.2f] from [%d] to [%d]\n",
amount, from, to);
accounts[from] -= amount;
accounts[to] += amount;
sufficientFunds.signalAll();
} catch (InterruptedException e) {
System.out.println("Gave up the lock...");
Thread.currentThread().interrupt();
} finally {
bankLock.unlock();
}
}
@Override
public String toString() {
bankLock.lock();
try {
double sum = 0;
for (double account : accounts) {
sum += account;
}
return "Bank's total balance is [" + sum + "]";
} finally {
bankLock.unlock();
}
}
public int size() {
return accounts.length;
}
}
Some explanation: before transferring the money I lock the bank object and wait for the sufficientFunds condition in the while loop with appropriate condition statement. If the condition is signaled or timeout passes but the condition is not met more than three times this program gives up transferring the funds and prints "--- could not transfer money ---" message.
Here is the source code to run the example:
package locks;
public class LocksAndConditionsTest {
private static final int ACCOUNTS_COUNT = 10;
private static final double INITIAL_BALANCE = 1000;
public static final long DELAY = 500;
public static void main(String[] args) {
Bank bank = new Bank(ACCOUNTS_COUNT, INITIAL_BALANCE);
for (int i = 0; i < ACCOUNTS_COUNT; ++i) {
Thread t = new Thread(new TransferTask(bank, INITIAL_BALANCE));
t.start();
}
}
static class TransferTask implements Runnable {
Bank bank;
double maxAmount;
public TransferTask(Bank b, double maxAmount) {
this.bank = b;
this.maxAmount = maxAmount;
}
public void run() {
try {
while (true) {
int to = (int) (bank.size() * Math.random());
int from = (int) (bank.size() * Math.random());
if (from == to) {
if (to == 0) {
from = to + 1;
} else {
from = to - 1;
}
}
bank.transfer(to, from, maxAmount * Math.random());
Thread.sleep(DELAY);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
Here is the source code for the Bank using synchronized keyword and old wait / notify mechanism:
package locks;
class BankSynchronized extends Bank {
public BankSynchronized(int accounts, double initialBalance) {
super(accounts, initialBalance);
}
@Override
public synchronized void transfer(int to, int from, double amount) {
try {
System.out.printf(
"Checking funds [%.2f] on account [%d]: [%.2f]\n",
amount, from, accounts[from]);
int count = 0;
while (accounts[from] < amount) {
wait(5000);
count++;
if (count > 3) {
System.out.println("--- could not transfer money ---");
return;
}
}
System.out.printf(
"Transferring [%.2f] from [%d] to [%d]\n",
amount, from, to);
accounts[from] -= amount;
accounts[to] += amount;
notifyAll();
} catch (InterruptedException e) {
System.out.println("Gave up the lock...");
Thread.currentThread().interrupt();
}
}
@Override
public synchronized String toString() {
double sum = 0;
for (double account : accounts) {
sum += account;
}
return "Bank's total balance is [" + sum + "]";
}
}
To test this implementation just change
Bank bank = new Bank(ACCOUNTS_COUNT, INITIAL_BALANCE);to
Bank bank = new BankSynchronized(ACCOUNTS_COUNT, INITIAL_BALANCE);in the LocksAndConditionsTest class and start the main class again.
The behavior of these two implementations should be the same (well, in fact it should be similar according to the Math.random() usage).
Implicit locks and conditions (synchronized keyword) have some limitations:
- You cannot interrupt a thread that is trying to acquire the lock
- Having a single condition per lock can be inefficient
And when you take a look at the JDK documentation you can find this:
Condition
factors out theObject
monitor methods (wait
,notify
andnotifyAll
) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitraryLock
implementations. Where aLock
replaces the use ofsynchronized
methods and statements, aCondition
replaces the use of the Object monitor methods.
When to use Lock, Condition objects or synchronized methods:
- It is best to use neither Lock / Condition nor the synchronized keyword. In many situations, you can use one of the mechanisms of the java.util.concurrent package that do all the locking for you.
- If the synchronized keyword work for your situation, by all means, use it. You write less code and have less room for error.
- Use Lock / Condition if you specifically need the additional power that these constructs give you
As you can see the main conclusion is that you should avoid both mechanisms like fire. You should use java.util.concurrent features. More on this in the next article. Stay tuned.
References: JDK 6 API and Core Java 2, Volume II--Advanced Features (7th edition)