Check out the new USENIX Web site.
FeatureUSENIX

 

using java

Using Threads Within Applets

rao_prithvi

by Prithvi Rao
<prithvi@ux4.sp.cs.cmu.edu>

Prithvi Rao is the co-founder of KiwiLabs, which specializes in software engineering methodology and Java/CORBA training. He has also worked on the development of the MACH OS and a real-time version of MACH. He is an adjunct faculty at Carnegie Mellon and teaches in the Heinz School of Public Policy and Management.

Graphical user interfaces (GUIs) are pervasive in computing environments. The trend seems to be that for every application that can be run from the command line there is likely to be a GUI from which the user can run the same program. (Well, this is largely true, but not totally.) Another trend seems to be the move toward Integrated Development Environments (IDE). Some examples are Visual Cafe, Kawa, and JBuilder, all of which facilitate the development of Java programs with GUI-based infrastructures providing capabilities from project management, at some level, to debugging. Two reasons to do this are to facilitate the rapid development of applications and also to encourage development by nonexperts.

While I don't have a strong opinion, either in favor or against, regarding these trends, it is useful to examine how multithreading helps provide a better implementation of GUI-based IDEs. (Of course emacs is still significant for many practitioners.) One key metric that determines the success of IDEs is performance, and threads can facilitate this capability.

This article is a mini tutorial on threading, in which we will look at threads within applets. The information is not restricted to use within applets. We will look at the basic concept of threads and describe their lifecycle. We will create a second thread in a Java applet and utilize the Runnable interface. Although IDEs are not (currently) implemented using applets, the experience is nevertheless useful.

Multithreading

Java provides support for threads even if the underlying operating system on which it runs does not. The support for threads was partially driven by the need to support network-based applications in which separate threads are used to transmit audio and video simultaneously (to give one example of an application).

Threads provide a mechanism for performing several programming tasks concurrently within a running program. A thread is a path of execution within a running program. Each thread has its own set of "virtual registers," also known as the thread's context; it's own stack; and a priority value. The various threads that are created and run (we restrict the discussion in this article to single-CPU environments) can compete for the CPU and are therefore scheduled to run based on some scheduler implementation. The scheduler is responsible for switching among the various threads and so maintains a list of the "runnable" threads. This can create the illusion that multiple programs are executing at the same time, but in a single CPU this is not the case. Any thread can be suspended or preempted at any time.

Typically, most programs need to perform a variety of tasks. Some typical tasks: updating a screen, getting information from a database, accessing the network. In a single-threaded program, all these functions are typically performed serially (this is not strictly true, because the "other" way to provide threads is to use interrupts). For instance, if the network-access thread is blocked, then it makes good sense to run another thread to update the screen. Basically, multithreading can provide better throughput but does not necessarily improve the response time for a particular thread. So if your aim is to make one program more responsive than another, multithreading alone will not necessarily accomplish that goal. Read on!

It is also possible that multithreading can simplify the writing of some applications. This is true in cases where the blocking has to be prevented by using synchronous calls, and this can make programming a more arduous task. (Windows 3.1 is an example of this.) The main problem here is that it introduces the notion of "event handling," which is not trivial.

Java Threads

New users of Java may not know that the virtual machine itself uses threads. Specifically, the garbage collection (for which Java is well known) happens in a separate thread. The virtual machine does not concern itself with scheduling (other than providing priority queues) and relinquishes all scheduling decisions to the underlying operating system. We will not go deeply into this because it is not our focus here. In the event that the operating system does not provide thread support, a package called "green threads" is ported together with the Java Virtual Machine to provide a threads layer on top of the operating system. For this reason, threads can be supported on any operating system.

Thread Lifecycle

Threads are objects and as such are created the same way other Java objects are created.

Thread foo = new Thread();

Threads have four states:

  • New thread

  • Runnable

  • Dead

  • Blocked

    When a thread object is created, it is not "runnable" at that time. The thread has all the resources it requires to be runnable but will only do so after a call to its start() method is made. This causes the scheduler to call the thread's run method.

    It is likely that many threads will be in a "runnable" state simultaneously. For a single CPU all threads compete for the CPU and (hopefully) eventually get to run. Contrast this to the multiprocessor case, where several threads could run in parallel. From a design point of view it is reasonable to assume that any "runnable" thread can run at any time.

    A running thread can voluntarily give up the processor by calling yield(). It can also call sleep() if it wishes to stop for a specific amount of time.

    A "runnable" thread can block for a number of reasons:

  • The running thread called sleep() or wait().

  • Any running thread called suspend.

  • The running thread blocked (while performing I/O perhaps).

    A thread can die for one of two reasons:

  • It exits from its run method.

  • It is killed from another object that calls its stop() method.

    Threads and the Runnable Interface

    There are two ways to create a thread class:

  • Extend the Thread class and override the run() method.

  • Implement the Runnable interface.

    The obvious way to create a new thread class is to extend the Thread class and override the run() method. This does not work if it is necessary to extend another class (such as an applet class — Java supports multiple inheritance of interfaces, not classes). Any class can implement the Runnable interface while extending another class. The Runnable interface is defined as:

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

    A class that implements an interface must implement all the methods defined in that interface, so a class that implements the Runnable interface must define a run() method.

    When the thread's start() method is called, the Java Virtual Machine calls the thread's run() method. The code in the run() method is the only code that runs as a separate thread.

    Threads and Applets

    In this section we will examine the use of threads to run applets. First we write an applet class and then write a thread class. The code demonstrates the running of applets as threads.

       public class sampleApplet extends Applet implements Runnable {

       private Thread thread; /* define a private reference to Thread */

       public void start() {

         thread = new Thread(this); /* create a new thread */
         thread.start(); /* make this thread runnable */
       }

       public void run() {

         /* this method will run on a separate thread */

       }

       } /* End of this class */

       public class Thread implements Runnable {

       private Runnable target;

       public Thread(Runnable foo) {

         target = foo; /* constructor for this class */

       }

       public void run() {
         if (target != null)
         target.run();
       }

       } /* End of this class */

    In the above example we show how an applet class can implement the Runnable interface. The start() method of the applet creates a new thread by calling the constructor that takes a reference to the target Runnable object — in other words, a reference to an object that provides the run() method. In this case, the run() method is defined in the applet itself, so the start() method passes this (a reference to itself) to the Thread constructor. The Thread constructor stores this reference in an instance variable called target. The start() puts the new Thread object into a "runnable" state by calling the start() method.

    When a Thread object enters the "runnable" state, it becomes a candidate for scheduling. The first time it is scheduled, the scheduler calls its run() method; the run() method checks to see if the target class is not equal to null and then calls the run() method of the target object, that is, the run() method in sampleApplet. This allows sampleApplet to be run in separate threads. The body of the run() method will usually be an infinite loop. (Remember, you exit the thread if you return from run().)

    Controlling a Thread

    A thread can be "blocked" temporarily by calling its suspend() and resume() methods. The following shows a code segment to do this.

       boolean stopped;

       public boolean mouseDown(Event x) {

        if (stopped)
         thread.resume();
        else
         thread.suspend();
         stopped = !stopped;
        return true;
       }

    A thread can put itself to sleep for a specified amount of time by calling its sleep method and passing to it the time (in milliseconds) of the sleep duration.

    Thread.sleep(long time);

    This is useful to do in tight loops to prevent the thread from "hogging" the CPU. This may throw an InterruptedException, so there needs to be code to catch this exception.

    Threads can be terminated by calling stop(). This throws a ThreadDeath object, and this must also be handled. All cleanup from a thread exiting can be done in the "finally" clause of the object.

    An Applet Example

    The following example displays a running count in an applet window.

       public class Counter extends Applet implements Runnable {
        private Thread thread;

        private int count; ; /* set up a counter */

        public void start() {
         if (thread == null) {
          thread = new Thread(this); /* target = this */
          thread.start();
        }
       }

        public void stop() { /* stop this thread */
         thread.stop();
         thread = null;
    }

        public void run() {
         while (true) {
          count++; /* increment the count */
          repaint(); /* request screen refresh */

         try {
          Thread.sleep(10); /* sleep for 10 ms */
         } catch (InterruptedException e) {}
        }
       }
        public void paint(Graphics g) { /* display the count */
         g.drawString("Count = " + count, 10, 10);
        }
       }

    In this example, the applet's start() method checks the value of the instance variable thread to see if it has already been created; if not, the start() method creates it and passes the this parameter to the thread's constructor. The run() method in the Thread object calls the run() method in this applet.

    After the Thread object is created, a call is made to its start() method. Once this method returns, the applet's start() method returns control to the browser. In the meantime, the new thread will start executing the applet's run() method. If there was no call to sleep(), the thread could potentially hog the CPU and not relinquish it, causing starvation for other threads.

    Creating Multiple Threads

    We can create multiple threads by putting the run() method in a class of its own.

    public class Counter implements Runnable {
      private TextField foo;
      private int count;

      public Counter(TextField t) {
       foo = t;
      }

      public void run() {
       while (true) {
        count++;
       txt.setText(count);
       .......
      }
     }
    }

    public class CounterTest extends Applet {
      private TextField x1, x2;
      private Counter y1, y2;

      public void start() {

       y1 = new Counter(x1);
       y2 = new Counter(x2);
       new Thread(y1).start();
       new Thread(y2).start();

     }

    }

    A class can contain only one run() method, so if an applet needs to create more than one thread and each thread needs to be different, then each run() method must be in a class of its own. Any class can implement the Runnable interface, so this is not a problem. In the example above, the Counter class displays the running count in a TextField component. The constructor initializes the instance variable to refer to a TextField object. The run() method is similar to that in the previous example, except that the count is displayed in the TextField component.

    The CounterTest class (an applet) can create as many Counter objects as required. In this example there are two such threads. The init() method for this class is shown below; it creates the TextField components and adds them to the applet window.

     public void init() {
      x1 = new TextField(24);
      x2 = new TextField(24);
      add(x1);
      add(x2);
     }

    A separate Thread object must be associated with each Counter object. Consequently, when the Thread objects are created, we pass the reference to the appropriate Counter object to the Thread constructor. Then we call the thread's start() method. After the second Thread object is created, control is returned to the browser. In the meantime, the two TextField components will display a running count.

    Conclusion

    The simplicity with which a novice Java programmer can create multithreaded applications is an appealing feature of this language. In this article we have presented a brief tutorial on threads in Java. I consider this brief because although we examined some fundamental aspects of using threads, the complexity associated increases in a nonlinear fashion as you scale the number of threads in your application. Add to this the fact that assigning priorities is not a trivial design task, and thread synchronization adds yet another level of complexity. It is important to understand threads as they have been presented here before embarking on the other fronts.

    In future articles, to demonstrate their capabilities, we will examine thread priorities and synchronization in the context of other applications.


  •  

    ?Need help? Use our Contacts page.
    Last changed: 10 Dec. 1999 mc
    Using Java index
    ;login: index
    USENIX home