Tuesday, September 30, 2008

When to use synchronized or Lock / Condition objects?

In this post I will present when you should use synchronized keyword in your Java code and how you can easily exchange it with Java 5 concurrency features.

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 the Object monitor methods (wait, notify and notifyAll) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary Lock implementations. Where a Lock replaces the use of synchronized methods and statements, a Condition replaces the use of the Object monitor methods.


When to use Lock, Condition objects or synchronized methods:
  1. 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.

  2. If the synchronized keyword work for your situation, by all means, use it. You write less code and have less room for error.

  3. 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)

2 comments:

jkilgrow said...

It sounds like we are trying to achieve the same things in our blogs.
Keep up the good work!

Javin @ classpath in java said...

Nice article , you have indeed cover the topic with great details. I have also blogged my experience on java How Synchronization works in Java. let me know how do you find it.