Why should we use the condition and what is the difference between Blocked and Waiting

advertisements

There is an example in the book 'Core Java', it transfer money from one account to another. I don't know what is the usefulness of a condition? In the book, it tells us:

if we just lock and wait without condition, it gets a deadlock:

private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;

public void transfer(int from, int to, int amount) {
    bankLock.lock();
    try {
        while (accounts[from] < amount) {
            // wait...
        }
        // transfer funds . . .
    } finally {
        bankLock.unlock();
    }
}

Now, what do we do when there is not enough money in the account? We wait until some other thread has added funds. But this thread has just gained exclusive access to the bankLock, so no other thread has a chance to make a deposit.

When call

sufficientFunds.await();

the current thread is now deactivated and gives up the lock. This lets in another thread that can, we hope, increase the account balance.

Lock locks the code, and condition gives up the lock, but i don't know what is condition, why not just simple unlock the block when money is not enough? And what's the difference between the thread state Blocked and Waiting? block: the thread can not run; waiting: the thread can not run too. What is different?

another question:

while (accounts[from] < amount) {
   ...
   sufficientFunds.await();

Why not write if?

if (accounts[from] < amount) {
    ...


There's a particular requirement to the class Bank: if it's asked to transfer an amount of money from one account to another and there's not enough money on the source account, it must wait until enough money is deposited to make the transfer possible. You could run a loop that checks if it's enough money on every iteration and acquires the lock only when this condition is met:

while (true) {
    if (accounts[from] >= amount) {
        bankLock.lock();
        try {
            if (account[from] >= amount) {
                // do the transfer here...
                break;
            }
        } finally {
            bankLock.unlock();
        }
    }
}

But this approach:

  • squanders the CPU resources on constant checks (what it nobody deposits enough money in hours or days?)
  • looks bulky and not idiomatic
  • doesn't always work (the reason is out of the scope of this question, I can give a link to the explanation in the comments if you're interested in it)

So, you need some mechanism that tells that you're just waiting for some change in the account. It's wasteful to check the amount of money in an account again and again if nobody deposits money into it. And there's more – you also need to acquire the lock as soon as someone has deposited money, so you could exclusively check the new account state and make the decision on whether you can make your transfer or not.

You must also keep in mind, that deposit is not the only operation allowed on the account. There're also withdrawals, for instance. If someone made a withdrawal, there's no need to check your account on the possibility of transfer because we know for sure that it's even less money now. So, we want to be awaken on deposits but don't want to be awaken on the withdrawals. We need to separate them somehow. This is where conditions come to play.

Condition is an object implementing the Condition interface. It's just an abstraction that allows you to divide your lock/wait logic into parts. In our case, we could have two conditions: one for increasing of an account balance, another for decreasing (for instance, if someone is waiting of zeroing the bank account to close it):

sufficientFunds = bankLock.newCondition();
decreasedFunds = bankLock.newCondition();

Now, you don't need to make bazillion of checks in the loop, you can organise your program the way that allows you to only awake and check the account when someone funds an account:

private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;

public void transfer(int from, int to, int amount) {
    bankLock.lock();
    try {
        while (accounts[from] < amount) {
            sufficientFunds.await();
        }
        // transfer funds ...
        sufficientFunds.signalAll();
    } finally {
        bankLock.unlock();
    }
}

public void deposit(int to, int amount) {
    bankLock.lock();
    try {
        // deposit funds...
        sufficientFunds.signalAll();
    } finally {
        bankLock.unlock();
    }
}

So, lets have a look at what's happening here and answer your questions:

  1. transfer() is trying to acquire the bankLock. If someone already holds this lock, your thread becomes blocked by another thread until the lock is released.

  2. When another thread releases the lock, you acquire it and can check the state of the account. If it's not enough money on it, you decide to sit and wait for someone to put money into the account, so you call sufficienFunds.await(). It makes your thread waiting for something to happen. You decided to do that, not just was blocked because of another thread, and that's the difference between blocked and waiting. But you're right, in both cases your thread is not running, so the difference is more logical, not technical.

  3. Calling await() on a condition that was created on the bankLock releases this lock and makes it available to other threads to acquire and perform operations. Without releasing the lock, your thread would block all the operations in the bank and lead to a deadlock.

  4. Now, when someone makes any change on the account that increases an amount of money, it notifies the sufficientFunds condition, our transferring thread awakes and the loop can make another check. Here we have two things to look at. First, when our thread awakes, it automatically reacquires the lock, so we can be sure that we can make checks and modifications exclusively and safely. Second, it might occur that the new amount of money is still not enough to make the transfer (signalling on a condition only means that something has happened that might change the state you're waiting for, but doesn't guarantee that state). In this case, we must wait for the next deposit. That's why we must use a while loop, not a simple if.

  5. When finally the amount of money in the account is enough, we make our transfer. And we also call sufficienFunds.signalAll() after making transfer because we've increased the amount of money in the to account. If some other thread is waiting for this account to be funded, it acquires the lock and can make its work.

So, using locks and conditions allows you to organise your multithread program in a safe and efficient manner.