Study guide: Achieve strong performance with threads, Part 3

Brush up on Java terms, learn tips and cautions, review homework assignments, and read Jeff's answers to student questions

Glossary of terms

condition
A prerequisite for continued execution. The reason one thread waits and another thread notifies the waiting thread.
condition variable
A Boolean variable that determines whether or not a thread will wait.
green threads
Threads that a JVM creates and manages.
preempts
Interrupting something in favor of something else. For example, a time-sliced thread scheduler interrupts the currently running thread and assigns a new thread to the processor when the currently running thread's quantum expires.
native threads
Threads that an operating system creates and manages.
priority
The relative importance of a thread, which you express as an integer from a well-defined range of values.
priority inheritance
A technique for solving priority inversion. The technique involves the thread scheduler silently raising the priority of a thread holding a lock required by a higher-priority thread to match the higher-priority thread's priority.
priority inversion
A phenomenon by which a higher-priority thread waiting for a lock held by a lower-priority thread has its execution delayed by other threads whose priority exceeds the priority of the thread holding the lock but is less than the lock-waiting thread's priority.
processor starvation
The situation in which a thread never runs because its priority is less than or equal to the priority of other threads.
quantum
A uniform time interval given to each equal-priority thread by the thread scheduler. Once a thread's quantum expires (when an internal timer goes off), a new thread is scheduled.
thread scheduler
The software responsible for performing thread scheduling.
thread scheduling
The act of a thread scheduler selecting the next runnable thread for execution.
time-slicing
A technique for eliminating processor starvation among equal-priority threads by preventing each thread from monopolizing the processor. Time-slicing uses a timer to determine the length of time (the quantum) by which an equal-priority thread runs. Once the timer goes off, or the quantum expires, the thread scheduler preempts the currently running thread with another thread.
user threads
A synonym for green threads.

Tips and cautions

These tips and cautions will help you write better programs and save you from agonizing over why the compiler produces error messages.

Tips

  • Should you call setPriority(int priority) or yield()? Both methods affect threads similarly. However, setPriority(int priority) offers flexibility, whereas yield() offers simplicity. Also, yield() might immediately reschedule the yielding thread, which accomplishes nothing. I prefer setPriority(int priority), but you must make your own choice.
  • Think of a condition as the reason one thread waits and another thread notifies the waiting thread.
  • This article demonstrates notify() in the ProdCons2 program because only one thread waits for a condition to occur. In your programs, where more than one thread might simultaneously wait for the same condition to occur, consider using notifyAll(). That way, no waiting thread waits indefinitely.

Cautions

  • If a call is made to wait() or notify() from outside a synchronized context, either call results in an IllegalMonitorStateException.

Miscellaneous notes and thoughts

The source code to the article's ProdCons2 application reveals my use of an if decision statement instead of a loop statement (such as while) to test the writeable condition variable. For example, in ProdCons2's Shared class, you encounter if in the following setSharedChar(char c) method:

synchronized void setSharedChar (char c)
{
   if (!writeable)
      try
      {
         wait ();
      }
      catch (InterruptedException e) {}
   this.c = c;
   writeable = false;
   notify ();
}

Various developers oppose the use of if to test condition variables. Instead, they argue in support of loops. By following their advice, the previous method transforms into the following method:

synchronized void setSharedChar (char c)
{
   while (!writeable)
      try
      {
         wait ();
      }
      catch (InterruptedException e) {}
   this.c = c;
   writeable = false;
   notify ();
}

Is it better to use if or a loop (such as while)? For ProdCons2, either approach is fine—because I've carefully coded my programs to ensure that a thread sets the appropriate condition variable (on which another thread waits) prior to waking that thread. For complex programs, where you are uncertain that a waiting thread's condition variable will be set to an appropriate value prior to waking that thread, you should use a loop. That way, the just-woken thread can retest its condition variable and wait if that variable contains the wrong value.

Homework

Please complete the following exercise:

Many computer science textbooks introduce the bounded buffer problem—a classic computer science problem that teaches the concept of monitors. According to the bounded buffer problem, a writer thread produces multiple data items stored in a fixed-size memory buffer. When that buffer fills, the writer thread waits. The reader thread notifies the writer thread when the buffer empties. Conversely, the reader thread retrieves data items from the memory buffer. When that buffer empties, the reader thread waits. The writer thread notifies the reader thread when the buffer fills.

The following BB.java source code demonstrates the bounded buffer problem. However, there is a catch. I omitted the source code to the Buffer class. Your job is to complete this program by supplying appropriate code. Make sure to use the wait() and notify() methods:

// BB.java
class BB
{
   public static void main (String [] args)
   {
      Buffer buffer = new Buffer (5); // Buffer holds a maximum of 5 characters
      new Writer (buffer).start ();
      new Reader (buffer).start ();
   }
}
class Buffer
{
   // Supply appropriate code in this class
}
class Writer extends Thread
{
   private Buffer buffer;
   Writer (Buffer buffer)
   {
      this.buffer = buffer;
   }
   public void run ()
   {
      for (int i = 0; i < 26; i++)
          buffer.put ((char) ('A' + i));
      for (int i = 0; i < 26; i++)
          buffer.put ((char) ('a' + i));
      for (int i = 0; i < 10; i++)
          buffer.put ((char) ('0' + i));
      buffer.put ('*'); // Indicate end of data items
   }
}
class Reader extends Thread
{
   private Buffer buffer;
   Reader (Buffer buffer)
   {
      this.buffer = buffer;
   }
   public void run ()
   {
      char c;
      while ((c = buffer.get ()) != '*')
         System.out.print (c);
      System.out.print ('\n');
   }
}

Once you finish writing and compiling the code, run the program. You should see the following output:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789

Answers to last month's homework

Last month, I asked you two questions and gave you a program to write. My answers appear in red.

  • Can multiple threads access the same local variables or parameters? Why or why not?

    No. Each thread has its own private copy of local variables and parameters. That copy exists on the thread's method-call stack.

  • Why is synchronization important?

    Synchronization is important because multiple-thread access to critical code sections often results in inconsistencies that produce strange behavior.

  • The article cautions you about synchronizing the run() method. Write a program that demonstrates why you should not synchronize that method.
  • The following SyncRun source code demonstrates what happens when you synchronize the run() method. If you run this program, you will notice that either thread A or thread B runs to completion before the other thread. That is due to the running thread holding the lock until it exits run(). As a result, the other thread delays its execution of that method.

    // SyncRun.java
    class SyncRun implements Runnable
    {
       public static void main (String [] args)
       {
          SyncRun sr = new SyncRun ();
          Thread t1 = new Thread (sr);
          t1.setName ("A");
          Thread t2 = new Thread (sr);
          t2.setName ("B");
          t1.start ();
          t2.start ();
       }
       public synchronized void run ()
       {
          for (int i = 0; i < 10; i++)
          {
               System.out.println (Thread.currentThread ().getName ());
               try
               {
                   Thread.sleep ((int) (Math.random () * 1000));
               }
               catch (InterruptedException e)
               {
               }
          }
       }
    }