Chapter 3. Synchronization Techniques

In the previous chapter, we covered a lot of ground: we examined how to create and start threads, how to arrange for them to terminate, how to name them, how to monitor their life cycles, and so on. In the examples of that chapter, however, the threads that we examined were more or less independent: they did not need to share any data between them.

In this chapter, we look at the issue of sharing data between threads. Sharing data between threads is often hampered due to what is known as a race condition between the threads attempting to access the same data more or less simultaneously. In this chapter, we’ll look at the concept of a race condition as well as examining a mechanism that solves race conditions. We will see how this mechanism can be used not only to coordinate access to data, but also for many problems in which synchronization is needed between threads. Before we start, let’s introduce a few concepts.

A Banking Example

As an application designer for a major bank, we are assigned to the development team for the automated teller machine (ATM). As our first assignment, we are given the task of designing and implementing the routine that allows a user to withdraw cash from the ATM. A first and simple attempt at an algorithm may be as follows (see Figure 3.1 for the flow chart):

  1. Check to make sure that the user has enough cash in the bank account to allow the withdrawal to occur. If the user does not, then go to step 4.

  2. Subtract the amount withdrawn from the user’s account.

  3. Dispense the cash from the teller machine to the user.

  4. Print a receipt for the user.

Algorithm flow chart for ATM withdrawal

Figure 3-1. Algorithm flow chart for ATM withdrawal

Given this very simple algorithm, an implementation may be as follows:

public class AutomatedTellerMachine extends Teller {
    public void withdraw(float amount) {
        Account a = getAccount();
        if (a.deduct(amount))
            dispense(amount);
        printReceipt();
    }
}
 
public class Account {
    private float total;
    public boolean deduct(float t) {
        if (t <= total) {
            total -= t;
            return true;
        }
        return false;
    }
}

Of course, we are assuming that the Teller class and the getAccount(), dispense(), and printReceipt() methods have already been implemented. For our purposes, we are simply examining this algorithm at a high level, so these methods will not be implemented here.

During our testing, we run a few simple and short tests of the routine. These tests involve withdrawing some cash. In certain cases, we withdraw a small amount. In other cases, we withdraw a large amount. We withdraw with enough cash in the account to cover the transaction, and we withdraw without enough cash in the account to cover the transaction. In each case, the code works as desired. Being proud of our routine, we send it to a local branch for beta testing.

As it turns out, it is possible for two people to have access to the same account (e.g., a joint account). One day, a husband and wife both decide to empty the same account, and purely by chance, they empty the account at the same time. We now have a race condition: if the two users withdraw from the bank at the same time, causing the methods to be called at the same time, it is possible for the two ATMs to confirm that the account has enough cash and dispense it to both parties. In effect, the two users are causing two threads to access the account database at the same time.

There is a race condition because the action of checking the account and changing the account status is not atomic. Here we have the husband thread and the wife thread competing for the account:

  1. The husband thread begins to execute the deduct() method.

  2. The husband thread confirms that the amount to deduct is less than or equal to the total in the account.

  3. The wife thread begins to execute the deduct() method.

  4. The wife thread confirms that the amount to deduct is less than or equal to the total in the account.

  5. The wife thread performs the subtraction statement to deduct the amount, returns true, and the ATM dispenses her cash.

  6. The husband thread performs the subtraction statement to deduct the amount, returns true, and the ATM dispenses his cash.

The Java specification provides certain mechanisms that deal specifically with this problem. The Java language provides the synchronized keyword; in comparison with other threading systems, this keyword allows the programmer access to a resource that is very similar to a mutex lock. For our purposes, it simply prevents two or more threads from calling our deduct() method at the same time:

public class Account {
    private float total;
    public synchronized boolean deduct(float t) {
        if (t <= total) {
            total -= t;
            return true;
        }
        return false;
    }
}

By declaring the method as synchronized, if two users decide to withdraw cash from the ATM at the same time, the first user executes the deduct() method while the second user waits until the first user completes the deduct() method. Since only one user may execute the deduct() method at a time, the race condition is eliminated.

Under the covers, the concept of synchronization is simple: when a method is declared as synchronized, it must have a token, which we call a lock. Once the method has acquired this lock (we may also say the lock has been checked out or grabbed ), it executes the method and releases (we may also say returns) the lock once the method is finished. No matter how the method returns—including via an exception—the lock is released. There is only one lock per object, so if two separate threads try to call synchronized methods of the same object, only one can execute the method immediately; the other thread has to wait until the first thread releases the lock before it can execute the method.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset