Chapter 14. Advanced Thread Topics

In this chapter we will cover the advanced thread topics. Specifically, we will explain how threads synchronize with each other when they have data that they must pass to each other. That is, they cannot solve the problem merely by staying out of each other's way and ignoring each other.

In Chapter 13, we reviewed the easy parts of thread programming as items 1 and 2 in a list there. This chapter covers items 3 and 4 from the same list. These are the hard parts of thread programming. We are about to plunge into level 3: when threads need to exclude each other from running during certain times.

Mutually Exclusive Threads

Here's where threads start to interact with each other, and that makes life a little more complicated. In particular, we have threads that need to work on the same pieces of the same data structure.

These threads must take steps to stay out of each other's way so they don't each simultaneously modify the same piece of data, leaving an uncertain result. Staying out of each other's way is known as mutual exclusion. You'll understand better why mutual exclusion is necessary if we motivate the discussion with some code. You should download or type the example in and run it.

This code simulates a steam boiler. It defines some values (the current reading of, and the safe limit for, a pressure gauge), and then instantiates ten copies of a thread called “pressure,” storing them in an array. The pressure class models an input valve that wants to inject more pressure into the boiler. Each pressure object looks to see if we are within safe boiler limits, and if so, increases the pressure. The main routine concludes by waiting for each thread to finish (this is the join() statement) and then prints the current value of the pressure gauge. Here is the main routine:

public class p {
     static int pressureGauge=0;
     static final int safetyLimit = 20;

     public static void main(String[]args) {
          pressure []p1 = new pressure[10];
          for (int i=0; i<10; i++)  {
               p1[i] = new pressure();
               p1[i].start();
          }
          // the 10 threads are now running in parallel
          try{
               for (int i=0;i<10;i++)
                         p1[i].join();  // wait for thread to end
          } catch(Exception e){ }

          System.out.println(
               "gauge reads "+pressureGauge+", safe limit is 20");
     }
}

Now let's look at the pressure thread. This code simply checks whether the current pressure reading is within safety limits, and if it is, it waits briefly, then increases the pressure.

The following is the thread:

class pressure extends Thread {

     void RaisePressure() {
          if (p.pressureGauge < p.safetyLimit-15) {
               // wait briefly to simulate some calculations
               try{sleep(100);} catch (Exception e){}
               p.pressureGauge += 15;
          }  else
               ; // pressure too high -- don't do anything.
     }

     public void run() {
          RaisePressure();
     }
}

If you haven't seen this before, it should look pretty safe. After all, before we increase the pressure reading we always check that our addition won't push it over the safety limit. Stop reading at this point, type in the two dozen lines of code, and run them. The following is what you might see:

% java p
gauge reads 150, safe limit is 20

Although we always checked the gauge before increasing the pressure, it is over the safety limit by a huge margin! Better evacuate the area! So what is happening here?

The problem is a race condition

This is a classic example of what is called a data race or a race condition. A race condition occurs when two or more threads update the same value simultaneously. What you want to happen is:

  1. Thread 1 reads pressure gauge

  2. Thread 1 updates pressure gauge

  3. Thread 2 reads pressure gauge

  4. Thread 2 updates pressure gauge

But it might happen that thread 2 starts to read before thread 1 has updated, so the accesses take place in this order:

  1. Thread 1 reads pressure gauge

  2. Thread 2 reads pressure gauge

  3. Thread 1 updates pressure gauge

  4. Thread 2 updates pressure gauge

In this case, thread 2 reads an erroneous value for the gauge (too low), effectively missing the fact that thread 1 is in the middle of updating the value based on what it read. For this example we helped the data race to happen by introducing a tenth-of-a-second delay between reading and updating. But whenever you have different threads updating the same data, a data race can occur even in statements that follow each other consecutively. It can even occur in the middle of expression evaluation! At Sun, inside the OS kernel group, for years every time we prototyped a faster speed processor or a new multi-processor, we uncovered (and fixed) new data race problems in Solaris.

In this example, we have highlighted what is happening and rigged the code to exaggerate the effect, but in general data races are among the hardest problems to debug. They typically do not reproduce consistently and they leave no visible clues as to how data got into an inconsistent state.

To avoid data races, follow this simple rule: Whenever two threads access the same data, they must use mutual exclusion. You can optimize slightly, by allowing multiple readers at one instant. A reader and a writer must never be accessing the same data at the same time. Two writers must never be running at the same time. As the name suggests, mutual exclusion is a protocol for making sure that if one thread is touching some particular data, another is not. The threads mutually exclude each other in time.

In Java, thread mutual exclusion is built on objects. Every object in the system has its own semaphore (strictly speaking, this will only be allocated if it is used), so any object in the system can be used as the “turnstile” or “thread serializer” for threads. You use the synchronized keyword and explicitly or implicitly provide an object, any object, to synchronize on. The run-time system will take over and apply the code to ensure that, at most, one thread has locked that specific object at any given instant, as shown in Figure 14-1.

Figure 14-1. Mutually exclusive threads

image

Synchronized blocks

You can apply the synchronized keyword to class methods, to instance methods, or to a block of code. In each case, the mutex (mutual exclusion) lock of some named object is acquired, then the code is executed, then the lock is released. If the lock is already held by another thread, then the thread that wants to acquire the lock is suspended until the lock is released.

The Java programmer never deals with the low-level and error-prone details of creating, acquiring, and releasing locks, but only specifies the region of code and the object that must be exclusively held in that region. You want to make your regions of synchronized code as small as possible, because mutual exclusion really chokes performance. The following are examples of each of these alternatives of synchronizing over a class, a method, or a block, with comments on how the exclusion works.

Mutual exclusion over an entire class

This is achieved by applying the keyword synchronized to a class method (a method with the keyword static). Making a class method synchronized tells the compiler, “Add this method to the set of class methods that must run with mutual exclusion,” as shown in Figure 14-2. Only one static synchronized method for a particular class can be running at any given time, regardless of how many objects there are. The threads are implicitly synchronized using the class object.

Figure 14-2. Mutual exclusion over the static methods of a class

image

In the preceding pressure example, we can make RaisePressure a static synchronized method by changing its declaration to this:

static synchronized void RaisePressure() {

Since there is only one of these methods for the entire class, no matter how many thread objects are created, we have effectively serialized the code that accesses and updates the pressure gauge. Recompiling with this change and rerunning the code will give this result (and you should try it):

% java p
gauge reads 15, safe limit is 20

Mutual exclusion over a block of statements

This is achieved by attaching the keyword “synchronized” before a block of code. You also have to explicitly mention in parentheses the object whose lock must be acquired before the region can be entered. Reverting to our original pressure example, we could make the following change inside the method RaisePressure to achieve the necessary mutual exclusion:

void RaisePressure() {
    synchronized(O) {
      if (p.pressureGauge < p.safetyLimit-15) {
            try{sleep(100);} catch (Exception e){} // delay
            p.pressureGauge += 15;
      }  else ; // pressure too high -- don't add to it.
    }
}

We will also need to provide the object O that we are using for synchronization. This declaration will do fine:

static Object O = new Object();

We could use an existing object, but we do not have a convenient one at hand in this example. The fields pressureGauge and safetyLimit are ints, not Objects, otherwise either of those would be a suitable choice. It is always preferable to use the object that is being updated as the synchronization lock wherever possible. Recompiling with the change and rerunning the code will give the desired exclusion:

% java p
 gauge reads 15, safe limit is 20

Mutual exclusion over a method

This is achieved by applying the keyword “synchronized” to an ordinary (non-static) method. Note that in this case the object whose lock will provide the mutual exclusion is implicit. It is the this object on which the method is invoked.

synchronized void foo() { ...  }

This is equivalent to:

void foo() {
  synchronized (this) {
      ...
  }
}

Note that making the obvious change to our pressure example does not give the desired result!

// this example shows what will NOT work
synchronized void RaisePressure() {
      if (p.pressureGauge < p.safetyLimit-15) {
            try{sleep(100);} catch (Exception e){} // delay
            p.pressureGauge += 15;
      }  else ; // pressure too high -- don't add to it.
}

image

The reason is clear: The “this” object is one of the ten different threads that are created. Each thread will successfully grab its own lock, and there will be no exclusion between the different threads at all. Synchronization excludes threads working on the same object; it doesn't synchronize the same method on different objects.

Be sure you are clear on this critical point: Synchronized methods are useful when you have several threads that might invoke methods simultaneously on the same one object. It ensures that, at most, one of all the methods designated as synchronized will be invoked on that one object at any given instant.

In this case we have the reverse. We have one thread for each of several different objects calling the same method simultaneously. Some system redesign is called for here.

Note that synchronized methods all exclude each other, but they do not exclude a non-synchronized method, nor a (synchronized or non-synchronized) static (class) method from running.

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

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