Unraveling Threads: Using This Built-In Java Feature for Fun and Profit

By Qusay H. Mahmoud

Threads are not a new idea; they’ve been around for quite some time. However, only a few programmers have used them. Fortunately, Java has incorporated threads as part of the language. It’s not only made their use easy, but also effective and productive.

The Benefits of Threads

Threads give your program the ability to perform multiple tasks simultaneously, and to take advantage of multiple CPUs. That is, they present the illusion that two or more events are occurring at the same time; however, your computer (if equipped with one CPU) can execute only one thread at any given time. This illusion of simultaneous execution is the result of rapid switching from one thread to another. Each executes for a short time, then passes control to the next. (In multiprocessor machines, however, multiple threads can execute concurrently.)

As an example, suppose we want a program to read a line of text from the user, via the keyboard. An exceedingly simple program will block execution of other programs until a line of text is entered. This is all right if the program has nothing else to do. But if the program could be doing background work such as drawing or animating images, then we are wasting CPU time. The background work could be accomplished with threads while the program waits for user input.

Other benefits include increased application throughput and responsiveness, and the ability to use system resources more efficiently.

Thread Basics

A program with a single flow of control is called sequential. At any time in such a program, the computer is executing at a single point. On the other hand, a program with alternating (or multiple) points of execution — i.e. threads — is called concurrent.

The term thread derives from the phrase “thread of execution” in operating systems. At any instant, a single point of execution exists within a single thread. Threads can create and kill other threads. Newly created threads will run in the same address space, allowing them to share data.

Figure 1 depicts a sequential program. Such a program has four parts: Source Code, Global Data, Heap, and Stack. The Source Code is the statements and expressions of the program, translated into machine language. The Global Data holds the static variables of the program, which are usually declared at the top level. The Heap is the storage used by the new operator when allocating new objects. The Stack is the storage that holds all local variables, method arguments, and other information.

Figure 1: A sequential program.

Figure 2 shows a program with two threads. As you can see, the structure is the same except that each thread has its own stack — because each thread could call a different set of methods in a totally different order. However, all threads share the same Source Code, Global Data, and Heap storage. Thus, if a thread makes a change to a local variable, other threads will not be affected — because those variables are on the thread’s own stack.

Figure 2: A program with two threads.

The simple example in Figure 3 creates and starts two threads. (Don’t worry about the details for now; we’ll cover them next.) One thread reads from a network socket, while the other reads from a keyboard. Both run in parallel. If you compile and run the program, you’ll get output like this:

ReadingFromSocket:0
ReadingFromKeyboard:0
ReadingFromSocket:1
ReadingFromKeyboard:1
ReadingFromSocket:2
ReadingFromKeyboard:2

Don’t be surprised if your output looks a bit different. Threads are machine-dependent; their run order can’t be guaranteed, though their scheduling and priorities can be manipulated, as you’ll see.

class MyThread extends Thread {
  
  public MyThread(String name) {
    super(name);
  }

  public void run() {
    for (int i=0; i<3; i++) {
      System.out.println(getName() + ":" + i);
      try {
        sleep(500);
      }
      catch(InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public static void main(String argv[]) {
    MyThread t1 = new MyThread("ReadFromSocket");
    MyThread t2 = new MyThread("ReadFromKeyboard");
      t1.start();
    t2.start();
  }
}

Figure 3: Creating and starting two threads.

Creating and Starting a Thread

There are two ways to create a thread in Java. The first way is to create an object from a custom class (e.g. MyThread) that extends the Thread class. The MyThread class must override the run method of the Thread class by providing an implementation for it, as Figure 3 depicted.

Instantiating the class MyThread:

MyThread t1 = new MyThread("read");

will not make the t1 object start executing as a thread. We must call Thread’s start method to start the thread’s execution. Once called, this method will in turn call the run method; that’s where all the actions to be performed by the thread should be placed.

This technique has a drawback, because we must extend the Thread class. Java doesn’t support multiple inheritance, so we can’t write the multithreaded applet we need to extend both the Applet and Thread classes. For this very reason, Java supports another way of creating a thread — through an interface.

The Runnable interface is used for creating threads, and is defined as follows:

package java.lang;

public interface Runnable {
  public abstract void run();
}

The only method this interface defines is run, which is declared as abstract, meaning the implementor of this interface must provide an implementation for run. Using this interface, we can implement our earlier example (see Figure 4).

class MyThread implements Runnable {

  public void run() {
    for (int i=0; i<3; i++) {
      System.out.println(Thread.currentThread().getName() +
                         ":" + i);
      try {
        Thread.sleep(500);
      }
      catch(InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public static void main(String argv[]) {
    MyThread t1 = new MyThread();
    MyThread t2 = new MyThread();
    Thread ReadFromSocket = new Thread(t1);
    Thread ReadFromKeyboard = new Thread(t2);
    ReadFromSocket.start();
    ReadFromKeyboard.start();
  }
}

Figure 4: An alternate technique for multithreading.

The main difference between the two ways is that the class implementing the Runnable interface has access to the run method only, and not to all the nice, handy methods provided by the Thread class. But as mentioned previously, the Runnable interface earns its keep because Java doesn’t directly support multiple inheritance.

When using the Runnable interface to create a thread, we must refer to the class that implements the Runnable interface — in this case, MyThread. Now a thread can be created by calling the Thread class, or a subclass of it, with a Runnable object as target:

MyThread x = new MyThread();
Thread t1 = new Thread(x, "ReadFromSocket");
t1.start();

When the thread starts executing, it will call the run method in the corresponding class that implements the Runnable interface.

Putting a Thread to Sleep

If you have a drawing method that runs in a separate thread, you may want to control, for example, how fast the thread draws an object. This can be done by briefly suspending the thread each time it’s about to draw an object. You may pause a thread for a specific time by using the sleep method. The argument to sleep specifies the milliseconds of duration. sleep can throw an exception — InterruptedException — that you should catch. The code segment in Figure 5, for example, puts a thread to sleep for one second.

// More code goes here.
public void run() {
  // More code goes here.
  try {
    sleep(1000);
  }
  catch(InterruptedException e) {
    e.printStackTrace();
  }
  // More code goes here.
}
// Mode code goes here.

Figure 5: Making a thread take a short nap.

I’ve found sleep to be invaluable — not just for use with threads, but in other applications requiring a brief pause before calling another method. Make sure you call it as:

Thread.sleep(amount);

Controlling Threads

As we’ve already seen, the start method is used to bring a newly created thread to life. Three other methods are part of the Thread class, and allow you to control execution: stop, suspend, and resume. They operate on the current thread object, and thus take no arguments. As the name suggests, the stop method is used to stop and destroy a thread. The suspend and resume methods are used to arbitrarily pause and restart execution. Applying these methods is easy. To suspend a thread, use:

MyThread.suspend()

Later, resume the execution of the thread by calling the resume method:

MyThread.resume(); 

Changing Thread Priority

Threads will normally compete for processor time. To give one thread an advantage over others, you may want to change its priority. Priorities range from 1 to 10, where 1 is a low priority and 10 is a high priority. The Thread class defines three constants used to select a common priority. These are: MIN_PRIORITY (equal to 1), NORM_PRIORITY (equal to 5), and MAX_PRIORITY (equal to 10). When a thread is first created, it inherits a priority from its parent thread — usually NORM_PRIORITY.

You can obtain a Thread priority by using the getPriority method; you can change a priority by calling the setPriority method, which takes an integer argument that must be greater than 0 and less than or equal to 10. If you pass an argument less than 1 or greater than 10, an IllegalArgumentException will be thrown at run time.

Here’s an example of why you may want to prioritize threads: a client with two threads — a reader and a writer. The reader reads from the server and writes to the console, while the writer reads from the console and writes to the server. In such a case, there might be a shared-access problem at the console. To prevent this, you must give the reader a higher priority than the writer.

If a thread has a high priority, it may not allow other threads to receive processor time. Such a thread is called “selfish.” A nicer thread would sleep for a while, or yield so that other threads can run. This can be accomplished with the yield method, which gives other threads the opportunity to run.

Synchronization

One pitfall of threads is data sharing. If two or more threads have shared access (read and write) to a variable, then care must be taken to coordinate and synchronize data access. As an example, suppose Alice and Bob (threads personified) are sharing a checkbook. If they’re not careful, their checks might exceed the balance. This code segment demonstrates:

int balance;

boolean withdraw(int amt) {
  if (balance - amt >= 0) {
    balance = balance - amt;
    return true;
  }
  return false;
}

Figure 6 shows what might happen if Alice and Bob execute this code simultaneously: The balance could become negative. The (hypothetical) withdraw method needs to be synchronized such that only one thread at a time can execute the code. Providing mutual exclusion — that is, preventing simultaneous access to a shared resource — can be accomplished with the synchronized modifier:

int balance;

synchronized boolean withdraw(int amt) {
  if (balance - amt >= 0) {
    balance = balance - amt;
    return true;
  }
  return false;
}

This code can be executed by only one thread at a time. Also, synchronized does not affect objects; it simply coordinates the sharing of data. This code sample would have the same effect as the previous one:

int balance;

boolean withdraw(int amt) {
  synchronized(this) {
    if (balance - amount >= 0) {
      balance = balance - amt;
      return true;
    }
    return false;
  }
}
Alice Bob Balance
if (80 - 50) >= 0) 80
if (80 - 50 >= 0) 80
balance = balance - 50; 30
balance = balance - 50; -20

Figure 6: The trouble with Alice and Bob.

Conclusion

Java incorporates threads as part of the language, making their use easy and effective. Using threads may increase your applications’ performance, in terms of throughput and responsiveness. Just remember to watch for a few pitfalls in coordinating access to shared data and in keeping threads nice, not selfish.

The files referenced in this article are available for download from the Informant Web site at http://www.informant.com/ji/jinewupl.htm. File name: JI9712QM.ZIP.

Qusay H. Mahmoud is a Senior Software Engineer in the School of Computer Science at Carleton University, Ottawa, Canada. Before joining Carleton University, he worked as a Software Designer at Newbridge Networks. Qusay holds a B.Sc. in Data Analysis and a Masters degree in Computer Science, both from the University of New Brunswick. You can reach him at dejavu@acm.org