多執行緒: Multi-Threading

Many examples in this document are adapted from Java: How To Program , written by Deitel and Deitel, and Thinking in Java (2nd Edition), written by Bruce Eckel. All examples are solely used for educational purposes. Hopefully, I am not violating any copyright issue here. If so, please do email me.

Please install JDK 1.4 or later with Java Plugin to view this page. Also, this page is best viewed with browsers (for examples, Mozilla/Firefox 0.99 or later, IE 6.x or later) with CSS2 support. This document is provided as is. You are welcomed to use it for non-commercial purpose.
Written by: 國立中興大學資管系呂瑞麟 Eric Jui-Lin Lu

請勿轉貼
看其他教材

目錄

  1. What is thread?
  2. The Life Cycle of a Thread
  3. Characteristics of Multi-threaded Programming
  4. Our first Thread using java.lang.Thread
  5. Controlling Threads
    1. sleep() vs. yield()
  6. Synchronization
    1. Deadlock
    2. Runnable
    3. Multithreading and GUI
  7. Interthread Communication
  8. Thread Group

What is thread?

From Wikipedia, "Threads are similar to processes, in that both represent a single sequence of instructions executed in parallel with other sequences, either by time slicing or priority. Threads are a way for a program to split itself into two or more simultaneously running tasks.". An advantage of a multi-threaded program is that it can operate faster on computer systems that have multiple CPUs, or across a cluster of machines.

Threads are distinguished from traditional multi-tasking operating system processes in that processes are typically independent, carry considerable state information, have separate address spaces, and interact only through system-provided inter-process communication mechanisms. Multiple threads, on the other hand, typically share the state information of a single process, share memory and other resources directly. Context switching between threads in the same process is typically faster than context switching between processes. Therefore, we often call threads as light-weighted processes.

Operating systems generally implement threads in one of two ways: preemptive multithreading, or cooperative multithreading. Preemptive multithreading is generally considered the superior implementation, as it allows the operating system to determine when a context switch should occur. Cooperative multithreading, on the other hand, relies on the threads themselves to relinquish control once they are at a stopping point. This can create problems if a thread is waiting for a resource to become available.

The Life Cycle of a Thread

The life cycle of a thread is shown as follow:

  1. Born state: a new thread was just created
  2. Ready state: the thread remains in the Born state until the program invoke start() method. Now, the thread is ready to run.
  3. Running state: The thread will be starting to run() when it is assigned a processor and running (scheduled by the OS).
  4. Dead state: The thread has completed or exited, and eventually disposed of by system.
  5. Sleep state: The thread can be going to sleep when it invokes sleep() method. The thread sleeps for a set time interval then awakens. A sleeping thread can be awaken by interrupt().
  6. Blocked state: The thread transits to the Blocked state when it attemps to perform a task that cannot be completed immediately and must temporarily wait until that task can be completed.
Note that a blocked or sleeping thread cannot use a processor even when there is one available.

Characteristics of Multi-threaded Programming

The characteristics of multi-threaded programming are summaried as follows:
  1. The order of thread execution cannot be predicted.
  2. Shared objects may be competed among threads. Thus, if threads are not well designed, you may obtain hazard results.
  3. Threads may use variables independently, and are not forced to share the same data.
  4. Threads can be coordinated to accomplish a specific task.

Our First Thread using java.lang.Thread

The Java Virtual Machine allows an application to have multiple threads of execution running concurrently. The java.lang.Thread class provides methods to start, suspend, resume, and stop a thread, as well as control other aspects such as the priority of a thread or the name associated with it. Noted that, in the following example, there are actually three threads -- t1, t2, and the main thread.

When a Java Virtual Machine starts up, there is usually a single non-daemon thread (which typically calls the method named main of some designated class). The Java Virtual Machine continues to execute threads until either of the following occurs:

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

  // Run method is executed when thread first started
  public void run() {
    for(int i = 0; i<10; i++) {
      System.out.println(i + ": " + getName());
      try {
        Thread.sleep((long)(Math.random() * 1000));
      } catch (InterruptedException ie) {}
    }

    System.out.println (getName() + " is finished!");
  }

  // Main method to create and start threads
  // main method itself is also a thread which is usually a control thread
  public static void main(String args[]) {

    // Create first thread instance
    Thread t1 = new ExtendThreadDemo("Love me");

    // Create second thread instance
    Thread t2 = new ExtendThreadDemo("Love me not");

    /*
    // Make both threads daemon threads
    // a daemon thread is a thread that runs for the benefit of other threads
    // when only daemons threads remains in the program, the program exists.
    t1.setDaemon(true); t2.setDaemon(true);
    */

    // Start both threads
    // program cannot call run() directly
    t1.start(); t2.start();

    /*
    try {
      // Sleep for one second, to allow threads
      // time to display first message
      Thread.sleep(1000);
    } catch (InterruptedException ie) {}
    */

    System.out.println("main is DONE");
  }
}

Controlling Threads

  1. Interrupting a Thread
    public class SleepyHead extends Thread {
      // Run method is executed when thread first started
      public void run() {
        System.out.println ("I feel sleepy. Wake me in eight hours");
    		
        try {
          // Sleep for eight hours
          Thread.sleep( 1000 * 60 * 60 * 8 );
    
          System.out.println ("That was a nice nap");
        } catch (InterruptedException ie) {
          System.err.println ("Just five more minutes....");
      }
    }
    
    
      // Main method to create and start threads
      public static void main(String args[]) throws java.io.IOException {
        // Create a 'sleepy' thread
        Thread sleepy = new SleepyHead();
    		
        // Start thread sleeping
        sleepy.start();
    		
        // Prompt user and wait for input
        System.out.println ("Press enter to interrupt the thread");
        System.in.read();
    
        // Interrupt the thread
        sleepy.interrupt();
      }
    }
    

  2. Stopping a Thread: Sometimes it is necessary to terminate a thread before it completes its task. For example, users are allowed to cancel an operation by pressing "Stop" button in the browser.

    However, stop() had been deprecated since it makes thread unsafe. For details, Please refer to Why are Thread.stop, Thread.suspend, Thread.resume, and Runtime.runFinalizersOnExit Deprecated? in the Java Specification for JDK 1.4.2.

    public class StopMe extends Thread {
      // Run method is executed when thread first started
      public void run() {
        int count = 1;
    
        System.out.println ("I can count. Watch me go!");
    
        for (;;)		{
          // Print count and increment it
          System.out.print (count++ + " ");
    
          // Sleep for half a second
          try { Thread.sleep(500); } catch (InterruptedException ie) {}
        }
      }
    
    
      // Main method to create and start threads
      public static void main(String args[]) throws java.io.IOException {
        // Create and start counting thread
        Thread counter = new StopMe();
        counter.start();
    		
        // Prompt user and wait for input
        System.out.println ("Press any enter to stop the thread counting");
        System.in.read();
    
        // Interrupt the thread
        counter.stop();
      }
    }
    

  3. SafeStopMe.java
    The Java language allows threads that access shared variables to keep private working copies of the variables; this allows a more efficient implementation of multiple threads. These working copies need be reconciled with the master copies in the shared main memory only at prescribed synchronization points, namely when objects are locked or unlocked. The volatile keyword is used on variables that may be modified simultaneously by other threads. This warns the compiler to fetch them fresh each time, rather than caching them in registers. This also inhibits certain optimisations that assume no other thread will change the values unexpectedly. Since other threads cannot see local variables, there is never any need to mark local variables volatile.
    public class SafeStopMe extends Thread {
    
      // the variable must be volatile (or access to the variable must be 
      // synchronized)
      private static volatile Thread blinker;
    
      // Run method is executed when thread first started
      public void run() {
    
        Thread thisThread = Thread.currentThread();
    
        int count = 1;
        System.out.println ("I can count. Watch me go!");
    
        while (thisThread == blinker)            {
          // Print count and increment it
          System.out.print (count++ + " ");
    
          // Sleep for half a second
          try { Thread.sleep(500); } catch (InterruptedException ie) {}
        }
      }
    
    
      // Main method to create and start threads
      public static void main(String args[]) throws java.io.IOException {
        // Create and start counting thread
        blinker = new SafeStopMe();
        blinker.start();
    
        // Prompt user and wait for input
        System.out.println ("Press any enter to stop the thread counting");
        System.in.read();
    
        // Interrupt the thread
        blinker = null;
      }
    }
    

  4. Yielding CPU Time
    Sometimes, a thread might be waiting for an event to occur, or may be entering a section of code where releasing CPU time to another thread will improve system performance or the user experience. For example, while waiting for data to become available from an InputStream, a thread might yield CPU time instead of going to sleep. In this situation, the static yield() method can be used.

    Remember, too, that it is a static method that affects the currently running threads only -- an application cannot yield the CPU time of a specific thread.

  5. sleep() vs. yield() As quoted from Peter Chase, If you sleep(), the sleeping thread will do nothing for the duration of the sleep, even if no other thread is runnable. If no other process wants the CPU, either, then the CPU will go idle. In contrast, if you yield() when no other thread is runnable, the yielding thread will carry on processing almost straight away.

  6. Waiting Until a Thread is Dead
    public class WaitForDeath extends Thread {
      // Run method is executed when thread first started
      public void run() {
        System.out.println ("This thread feels a little ill....");
    
        // Sleep for five seconds
        try { 
          Thread.sleep(5000);
        } catch (InterruptedException ie) {}
      }
    
      // Main method to create and start threads
      public static void main(String args[]) throws java.lang.InterruptedException {
        // Create and start dying thread
        Thread dying = new WaitForDeath();
        dying.start();
    		
        // Prompt user and wait for input
        System.out.println ("Waiting for thread death");
    
        // Wait till death
        dying.join();
    
        System.out.println ("Thread has died");
      }
    }
    

  7. Waiting until a Thread is dead
    public class WaitForDeath extends Thread {
      public WaitForDeath (String name) {
        super(name);
      }
    
      // Run method is executed when thread first started
      public void run() {
        // Sleep for a while
        try { 
          Thread.sleep((long)(Math.random() * 3000));
        } catch (InterruptedException ie) {}
        System.out.println(getName() + " has died.");
      }
    
    
      // Main method to create and start threads
      public static void main(String args[]) throws java.lang.InterruptedException {
        // Create and start dying thread
        Thread dying1 = new WaitForDeath("First thread");
        dying1.start();
        Thread dying2 = new WaitForDeath("Second thread");
        dying2.start();
    		
        // Prompt user and wait for input
        System.out.println ("Waiting for thread death");
    
        // Wait till death
        dying1.join(); dying2.join();
    
        System.out.println ("main thread has died");
      }
    }
    
  8. Assigning a Thread Priority
    In Java, a numerical ranking specifies thread priority, with 10 (or defined as Thread.MAX_PRIORITY) being the highest priority type and 1 (or defined as Thread.MIN_PRIORITY) being the lowest. If not specifically assigned, a thread is running as normal thread with priority number 5 (or defined as Thread.NORM_PRIORITY). NOTE that the priority value is a only suggest value to the operation system to indicate which thread is more important and should be scheduled more frequently.

    1. PriorityDemo2.java
      // Please run the program and observe the execution order.
      // If your CPU speed is faster, you may want to increase the number
      // of loops in the following program.
      public class PriorityDemo2 extends Thread {
        public PriorityDemo2(String name) {
          super(name);
        }
      
        // Run method is executed when thread first started
        public void run() {
          int p;
          for(int i=0; i<100; i++) {
            p = getPriority();
      
            if(p == 5 && i < 10)
              System.out.println(i + ": " + getName() + " with priority " + 
                                 p + " is done!");
      
            if(p == 6 && i > 90)
              System.out.println(i + ": " + getName() + " with priority " + 
                                 p + " is done!");
          }
        }
      
        // Main method to create and start threads
        // main method itself is also a thread which is usually a control thread
        public static void main(String args[]) {
      
          Thread t1 = new PriorityDemo2("Love me");
          Thread t2 = new PriorityDemo2("Love me not");
          Thread t3 = new PriorityDemo2("You Guess");
      
          // set priority
          t1.setPriority(4); t3.setPriority(6);
      
          // Start both threads
          // program cannot call run() directly
          t1.start(); t2.start(); t3.start();
      
          System.out.println("main is DONE");
        }
      }
      

      The following box contains a running result from a Solaris machine. However, you may obtain totally different result on a Win32 machine. This is due to the fact that scheduling is handled by operation systems.

      dns:~/java> java PriorityDemo2
      main is DONE
      0: Love me not with priority 5 is done!
      1: Love me not with priority 5 is done!
      2: Love me not with priority 5 is done!
      3: Love me not with priority 5 is done!
      4: Love me not with priority 5 is done!
      5: Love me not with priority 5 is done!
      6: Love me not with priority 5 is done!
      7: Love me not with priority 5 is done!
      8: Love me not with priority 5 is done!
      9: Love me not with priority 5 is done!
      91: You Guess with priority 6 is done!
      92: You Guess with priority 6 is done!
      93: You Guess with priority 6 is done!
      94: You Guess with priority 6 is done!
      95: You Guess with priority 6 is done!
      96: You Guess with priority 6 is done!
      97: You Guess with priority 6 is done!
      98: You Guess with priority 6 is done!
      99: You Guess with priority 6 is done!
      

    2. PriorityDemo.java
      public class PriorityDemo extends Thread {
        public PriorityDemo (String name) {
          super(name);
        }
      
        // Run method is executed when thread first started
        public void run() {
          for(int i = 0; i<10; i++) {
            int p = getPriority();
            System.out.println(i + ": " + getName() + "; " + p);
            p = (p + 1) % 10;
            if (p == 0) p++;
            setPriority(p);
            try {
              Thread.sleep((long)(Math.random() * 1000));
            } catch (InterruptedException ie) {}
          }
      
          System.out.println (getName() + " is finished!");
        }
      
        public static void main(String args[]) {
      
          Thread t1 = new PriorityDemo("Love me");
          Thread t2 = new PriorityDemo("Love me not");
          Thread t3 = new PriorityDemo("You Guess");
      
          // set priority
          t1.setPriority(Thread.MIN_PRIORITY); t2.setPriority(4); 
          t3.setPriority(Thread.MAX_PRIORITY);
      
          // Start both threads
          // program cannot call run() directly
          t1.start(); t2.start(); t3.start();
      
          System.out.println("main is DONE");
        }
      }
      

Synchronization

As we knew, each thread runs independently and may be extected at different speed. Thus, it is very difficult to predict their execution order. This will cause no problem if these threads are independent and the running result remains the same no matter which thread is executed first. However, if there are many threads competing for a shared resource, and if these threads are not coordinated properly, wierd results may be obtained.

In Java, to avoid unpredicted results, thread synchronization is used. When a thread accessing the shared resource excludes all other thread from doing so simultaneously, we call it mutual exclusion or thread synchronization.

  1. First of all, let's take a look at an example that has no synchronization. We use the concept of producer/consumer to illustrate the problem. In the example, the producer places data into the shared object, and the consumer retrieve data from the shared object. If the threads are designed properly, the consumer will read 1, 2, 3, and 4 from the shared object (which was written by the producer in sequence) and the result will be 10.
    1. Buffer.java
      // Fig. 16.4: Buffer.java
      // Buffer interface specifies methods called by Producer and Consumer.
      
      public interface Buffer {
         public void set( int value );  // place value into Buffer
         public int get();              // return value from Buffer
      }
        

    2. UnsynchronizedBuffer.java
      // Fig. 16.7: UnsynchronizedBuffer.java
      // UnsynchronizedBuffer represents a single shared integer.
      
      public class UnsynchronizedBuffer implements Buffer {
         private int buffer = -1; // shared by producer and consumer threads
      
         // place value into buffer
         public void set( int value ) {
            System.err.println( Thread.currentThread().getName() +
               " writes " + value );
      
            buffer = value;
         }
      
         // return value from buffer
         public int get() {
            System.err.println( Thread.currentThread().getName() +
               " reads " + buffer );
      
            return buffer; 
         }
      }
        

    3. Producer.java
      // Fig. 16.5: Producer.java
      // Producer's run method controls a thread that
      // stores values from 1 to 4 in sharedLocation.
      
      public class Producer extends Thread {
         private Buffer sharedLocation; // reference to shared object
      
         // constructor
         public Producer( Buffer shared ) {
             super( "Producer" );
             sharedLocation = shared;
         }
      
         // store values from 1 to 4 in sharedLocation
         public void run() {
            for ( int count = 1; count <= 4; count++ ) {  
               
               // sleep 0 to 3 seconds, then place value in Buffer
               try {
                  Thread.sleep( ( int ) ( Math.random() * 3001 ) );
                  sharedLocation.set( count );  
               }
      
               // if sleeping thread interrupted, print stack trace
               catch ( InterruptedException exception ) {
                  exception.printStackTrace();
               }
      
            } // end for
      
            System.err.println( getName() + " done producing." + 
               "\nTerminating " + getName() + ".");
      
         } // end method run
      } // end class Producer
        

    4. Consumer.java
      // Fig. 16.6: Consumer.java
      // Consumer's run method controls a thread that loops four
      // times and reads a value from sharedLocation each time.
      
      public class Consumer extends Thread { 
         private Buffer sharedLocation; // reference to shared object
      
         // constructor
         public Consumer( Buffer shared ) {
            super( "Consumer" );
            sharedLocation = shared;
         }
      
         // read sharedLocation's value four times and sum the values
         public void run() {
            int sum = 0;
      
            for ( int count = 1; count <= 4; count++ ) {
               
               // sleep 0 to 3 seconds, read value from Buffer and add to sum
               try {
                  Thread.sleep( ( int ) ( Math.random() * 3001 ) );    
                  sum += sharedLocation.get();
               }
      
               // if sleeping thread interrupted, print stack trace
               catch ( InterruptedException exception ) {
                  exception.printStackTrace();
               }
            }
      
            System.err.println( getName() + " read values totaling: " + sum + 
               ".\nTerminating " + getName() + ".");
      
         } // end method run
      
      } // end class Consumer
        

    5. SharedBufferTest.java
      // Fig. 16.8: SharedBufferTest.java
      // SharedBufferTest creates producer and consumer threads.
      
      public class SharedBufferTest {
      
          public static void main( String [] args ) {
              // create shared object used by threads
              Buffer sharedLocation = new UnsynchronizedBuffer();
      
              // create producer and consumer objects
              Producer producer = new Producer( sharedLocation );
              Consumer consumer = new Consumer( sharedLocation );
      
              producer.start();  // start producer thread
              consumer.start();  // start consumer thread
      
          } // end main
      
      } // end class SharedCell
        

    6. Sample Running Results
      C:\tmp\examples\ch16\fig16_04_08>java SharedBufferTest
      Consumer reads -1
      Consumer reads -1
      Producer writes 1
      Producer writes 2
      Producer writes 3
      Consumer reads 3
      Producer writes 4
      Producer done producing.
      Terminating Producer.
      Consumer reads 4
      Consumer read values totaling: 5.
      Terminating Consumer.
      
      C:\tmp\examples\ch16\fig16_04_08>java SharedBufferTest
      Consumer reads -1
      Producer writes 1
      Producer writes 2
      Consumer reads 2
      Consumer reads 2
      Consumer reads 2
      Consumer read values totaling: 5.
      Terminating Consumer.
      Producer writes 3
      Producer writes 4
      Producer done producing.
      Terminating Producer.
        
  2. The same example with synchronization
    Java uses monitors to perform synchronization. Every object has a monitor which will only allow a thread to execute inside a synchronized statement of the object at a time. This is accomplished by locking the object when the program enters the synchronized statement. All other threads attempting to enter the synchronized statement on the same object are placed in the Blocked state.

    To specified the synchronized statement, Java uses the keyword synchronized for a method or a block of code. As a designer, you MUST ensure dead lock will never occur.

    1. SynchronizedBuffer.java
      // Fig. 16.9: SynchronizedBuffer.java
      // SynchronizedBuffer synchronizes access to a single shared integer.
      
      public class SynchronizedBuffer implements Buffer {
         private int buffer = -1; // shared by producer and consumer threads
         private int occupiedBufferCount = 0; // count of occupied buffers
         
         // place value into buffer
         public synchronized void set( int value ) {
            // for output purposes, get name of thread that called this method
            String name = Thread.currentThread().getName();
      
            // while there are no empty locations, place thread in waiting state
            while ( occupiedBufferCount == 1 ) {
               
               // output thread information and buffer information, then wait
               try {
                  System.err.println( name + " tries to write." );
                  displayState( "Buffer full. " + name + " waits." );
                  wait();
               }
      
               // if waiting thread interrupted, print stack trace
               catch ( InterruptedException exception ) {
                  exception.printStackTrace();
               }
      
            } // end while
              
            buffer = value; // set new buffer value
              
            // indicate producer cannot store another value
            // until consumer retrieves current buffer value
            ++occupiedBufferCount;
              
            displayState( name + " writes " + buffer );
            
            // Be aware of notify(). It will notify one thread to wake up,
            // but cannot ask one specific thread to wake up. So, if you
            // not sure how many producer threads are waiting, it is better
            // to use notifyAll() to wake all waiting threads up.
            notify(); // tell waiting thread to enter ready state
              
         } // end method set; releases lock on SynchronizedBuffer 
          
         // return value from buffer
         public synchronized int get() {
            // for output purposes, get name of thread that called this method
            String name = Thread.currentThread().getName();
      
            // while no data to read, place thread in waiting state
            while ( occupiedBufferCount == 0 ) {
      
               // output thread information and buffer information, then wait
               try {
                  System.err.println( name + " tries to read." );
                  displayState( "Buffer empty. " + name + " waits." );
                  wait();
               }
      
               // if waiting thread interrupted, print stack trace
               catch ( InterruptedException exception ) {
                  exception.printStackTrace();
               }
      
            } // end while
      
            // indicate that producer can store another value 
            // because consumer just retrieved buffer value
            --occupiedBufferCount;
      
            displayState( name + " reads " + buffer );
            
            notify(); // tell waiting thread to become ready to execute
      
            return buffer;
      
         } // end method get; releases lock on SynchronizedBuffer 
          
         // display current operation and buffer state
         public void displayState( String operation ) {
            StringBuffer outputLine = new StringBuffer( operation );
            outputLine.setLength( 40 );
            outputLine.append( buffer + "\t\t" + occupiedBufferCount );
            System.err.println( outputLine );
            System.err.println();
         }
          
      } // end class SynchronizedBuffer
        

    2. SharedBufferTest2.java
      // Fig. 16.10: SharedBufferTest2.java
      // SharedBufferTest2creates producer and consumer threads.
      
      public class SharedBufferTest2 {
      
         public static void main( String [] args ) {
            // create shared object used by threads; we use a SynchronizedBuffer
            // reference rather than a Buffer reference so we can invoke 
            // SynchronizedBuffer method displayState from main
            SynchronizedBuffer sharedLocation = new SynchronizedBuffer();
              
            // Display column heads for output
            StringBuffer columnHeads = new StringBuffer( "Operation" );
            columnHeads.setLength( 40 );
            columnHeads.append( "Buffer\t\tOccupied Count" );
            System.err.println( columnHeads );
            System.err.println();
            sharedLocation.displayState( "Initial State" );
              
            // create producer and consumer objects
            Producer producer = new Producer( sharedLocation );
            Consumer consumer = new Consumer( sharedLocation );
              
            producer.start();  // start producer thread
            consumer.start();  // start consumer thread
              
         } // end main
      } // end class SharedBufferTest2
        

    練習題: 假設我們無法使用數學公式來計算從 1 加到 1000000, 請寫一個多執行緒的程式來計算 1 加到 1000000 的總合。你可以產生 四個 threads 來執行計算。完成後,請與只有單執行緒的情形作比較, 看看它們的執行效率是否相同。在試試看能不能由四個 threads 都擁有 全部的總合!

  3. Block-level Synchronization Before we demo a deadlock example, the block-level synchronization will be shown first.
    public class SynchBlock extends Thread {
      static StringBuffer buffer;
      static int counter;
    
      public SynchBlock(String s) {
        super(s);
        buffer = new StringBuffer();
        counter= 1;
      }
    
      public void run() {
    
        // synchronization lock against a particular object -- buffer
        // 其實可以作的更精緻一點,counter 和 buffer 分開 lock
        synchronized (buffer) {
          int tempVariable = counter++;
    
          // Create message to add to buffer, including linefeed
          String message = getName() + ": Count value is : " + tempVariable +
                           System.getProperty("line.separator");
    
          try {
            Thread.sleep(100);
          } catch (InterruptedException ie) {}
    
          buffer.append (message);
        }
      }
    
      public static void main(String args[]) throws Exception {
        // Create a new runnable instance
        Thread t1 = new SynchBlock("Thread 1");
        Thread t2 = new SynchBlock("Thread 2");
        Thread t3 = new SynchBlock("Thread 3");
        Thread t4 = new SynchBlock("Thread 4");
        t1.start(); t2.start(); t3.start(); t4.start();
    
        // Wait for all three threads to finish
        t1.join(); t2.join(); t3.join(); t4.join();
    
        System.out.println (buffer);
      }
    }
    

  4. A Simple Deaklock Example
    If you need to know more about deadlocks in multithreading programming, it is advised to read Programming Java threads in the real world, Part2: The perils of race conditions, deadlock, and other threading problems. Note that, in the real world programs, it is not easy to see the deadlock situation until you hit it.
    public class Deadlock {
      static Lock lock1;
      static Lock lock2;
      static int  state;
    
      public static void main(String[] args) {
        lock1 = new Lock();
        lock2 = new Lock();
        Process1 p1 = new Process1();
        Process2 p2 = new Process2();
        p1.start();
        p2.start();
      }
    }
    
    class Lock {}
    
    class Process1 extends Thread {
      public void run() {
        Deadlock.state++;
        // lock Deaklock.lock1 first and then try to lock Deadlock.lock2
        while(true) {
          System.out.println("Process 1: " + Deadlock.state);
          synchronized (Deadlock.lock1) {
            try {
              Thread.sleep(( int ) ( Math.random() * 100));
            } catch (InterruptedException exception ) {
              exception.printStackTrace();
            }
    
            synchronized (Deadlock.lock2) {
              Deadlock.state++;
            }
          }
        }
      }
    }
    
    class Process2 extends Thread {
      public void run() {
        Deadlock.state++;
        // lock Deaklock.lock2 first and then try to lock Deadlock.lock1
        while(true) {
          System.out.println("Process 2: " + Deadlock.state);
          synchronized (Deadlock.lock2) {
            try {
              Thread.sleep(( int ) ( Math.random() * 100));
            } catch (InterruptedException exception ) {
              exception.printStackTrace();
            }
    
            synchronized (Deadlock.lock1) {
              Deadlock.state++;
            }
          }
        }
      }
    }
    

  5. Runnable
    There are situations that a class needs to be a derived class of a specific class and also be a thread. However, since Java does not support multiple inheritance, Java uses interface Runnable for this purpose.

    Implementing the Runnable interface in a class enables a program to manipulate objects of that class as Runnable objects. A program that uses a Runnable object to control a thread creates a Thread object and associates the Runnable object with that Thread.

    // java.lang.Thread implements java.lang.Runnable
    class RunnableThread implements Runnable {
      // Run method is executed when thread first started
      public void run() {
        System.out.println (Thread.currentThread().getName() + 
                            " is an instance of the Runnable interface");
      }
    }
    
    public class RunnableThreadDemo {
      // Main method to create and start threads
      public static void main(String args[]) {
        System.out.println ("Creating runnable object");
        RunnableThread r = new RunnableThread();
    
        // Create a thread, and pass the runnable object
        System.out.println ("Creating first thread");
        Thread t1 = new Thread(r, "Thread 1");
    
        // Create a second thread, and pass the runnable object
        System.out.println ("Creating second thread");
        Thread t2 = new Thread(r, "Thread 2");
    
        // Start both threads
        System.out.println ("Starting both threads");
        t1.start(); t2.start();
      }
    }
    

  6. Multithreading and GUI:
    In this example, the producer and the consumer will work on a circular buffer and then display its current status on an JTextArea object. This is the first program that allows multiple threads update there status on Swing GUI components.

    However, Swing components are not thread-safe -- if multiple threads manupulate a Swing GUI component, the result may not be correct. Thus, all interactions with Swing components should be performed one at a time. Normally, the so-called event-dispatching thread (also known as the event-handling thread) is in charge of the management of GUI component processing. In Java, the class javax.swing.SwingUtilities provides a static method invokeLater() so that a GUI processing thread (which implements Runnable) to be executed later as part of the event-dispatching thread.

    1. CircularBuffer.java
      // Fig. 16.14: CircularBuffer.java
      // CircularBuffer synchronizes access to an array of shared buffers.
      import javax.swing.*;
      
      public class CircularBuffer implements Buffer {
      
         // each array element is a buffer 
         private int buffers[] = { -1, -1, -1 };
          
         // occupiedBufferCount maintains count of occupied buffers
         private int occupiedBufferCount = 0;
          
         // variables that maintain read and write buffer locations
         private int readLocation = 0, writeLocation = 0;
          
         // reference to GUI component that displays output
         private JTextArea outputArea;
          
         // constructor
         public CircularBuffer( JTextArea output ) {
            outputArea = output;
         }
         
         // place value into buffer
         public synchronized void set( int value ) {
            // for output purposes, get name of thread that called this method
            String name = Thread.currentThread().getName();
      
            // while there are no empty locations, place thread in waiting state
            while ( occupiedBufferCount == buffers.length ) {
               
               // output thread information and buffer information, then wait
               try {
                  SwingUtilities.invokeLater( new RunnableOutput( outputArea, 
                     "\nAll buffers full. " + name + " waits." ) );                
                  wait();
               }
      
               // if waiting thread interrupted, print stack trace
               catch ( InterruptedException exception )
               {
                  exception.printStackTrace();
               }
      
            } // end while
              
            // place value in writeLocation of buffers
            buffers[ writeLocation ] = value;
              
            // update Swing GUI component with produced value
            SwingUtilities.invokeLater( new RunnableOutput( outputArea, 
               "\n" + name + " writes " + buffers[ writeLocation ] + " ") );
              
            // just produced a value, so increment number of occupied buffers
            ++occupiedBufferCount;
              
            // update writeLocation for future write operation
            writeLocation = ( writeLocation + 1 ) % buffers.length;
              
            // display contents of shared buffers
            SwingUtilities.invokeLater( new RunnableOutput( 
               outputArea, createStateOutput() ) );
              
            notify(); // return waiting thread (if there is one) to ready state
              
         } // end method set
         
         // return value from buffer
         public synchronized int get() {  
            // for output purposes, get name of thread that called this method
            String name = Thread.currentThread().getName();
      
            // while no data to read, place thread in waiting state
            while ( occupiedBufferCount == 0 ) {
              
               // output thread information and buffer information, then wait
               try {
                  SwingUtilities.invokeLater( new RunnableOutput( outputArea, 
                     "\nAll buffers empty. " + name + " waits.") );
                  wait();
               }
                  
               // if waiting thread interrupted, print stack trace
               catch ( InterruptedException exception ) {
                  exception.printStackTrace();
               }
      
            } // end while
              
            // obtain value at current readLocation
            int readValue = buffers[ readLocation ];
              
            // update Swing GUI component with consumed value
            SwingUtilities.invokeLater( new RunnableOutput( outputArea, 
               "\n" + name + " reads " + readValue + " ") );
              
            // just consumed a value, so decrement number of occupied buffers
            --occupiedBufferCount;
              
            // update readLocation for future read operation
            readLocation = ( readLocation + 1 ) % buffers.length;
      
            // display contents of shared buffers
            SwingUtilities.invokeLater( new RunnableOutput( 
               outputArea, createStateOutput() ) );
                   
            notify(); // return waiting thread (if there is one) to ready state
              
            return readValue;
              
         } // end method get
          
         // create state output
         public String createStateOutput() {
            // first line of state information
            String output = 
               "(buffers occupied: " + occupiedBufferCount + ")\nbuffers: ";
      
            for ( int i = 0; i < buffers.length; i++ )
               output += " " + buffers[ i ] + "  ";
      
            // second line of state information
            output += "\n         ";
      
            for ( int i = 0; i < buffers.length; i++ )
               output += "---- ";
      
            // third line of state information
            output += "\n         ";
      
            // append readLocation (R) and writeLocation (W)
            // indicators below appropriate buffer locations
            for ( int i = 0; i < buffers.length; i++ )
      
               if ( i == writeLocation && writeLocation == readLocation )
                  output += " WR  ";
               else if ( i == writeLocation )
                  output += " W   ";
               else if ( i == readLocation )
                  output += "  R  ";
               else 
                  output += "     ";
      
            output += "\n";
      
            return output;
      
         } // end method createStateOutput
      } // end class CircularBuffer
        

    2. RunnableOutput.java
      // Fig. 16.11: RunnableOutput.java
      // Class RunnableOutput updates JTextArea with output
      import javax.swing.*;
      
      public class RunnableOutput implements Runnable {
         private JTextArea outputArea;
         private String messageToAppend;
          
         // initialize outputArea and message
         public RunnableOutput( JTextArea output, String message ) {
            outputArea = output;
            messageToAppend = message;
         }
          
         // method called by SwingUtilities.invokeLater to update outputArea
         public void run() {
            outputArea.append( messageToAppend );
         }
      } // end class RunnableOutput
        

    3. Producer.java
      // Fig. 16.12: Producer.java
      // Producer?? run method controls a thread that 
      // stores values from 11 to 20 in sharedLocation.
      import javax.swing.*;
      
      public class Producer extends Thread {
         private Buffer sharedLocation;
         private JTextArea outputArea;
          
         // constructor
         public Producer( Buffer shared, JTextArea output ) {
            super( "Producer" );
            sharedLocation = shared;
            outputArea = output;
         }
          
         // store values from 11-20 and in sharedLocation's buffer
         public void run() {
            for ( int count = 11; count <= 20; count ++ ) {
              
               // sleep 0 to 3 seconds, then place value in Buffer
               try {
                  Thread.sleep( ( int ) ( Math.random() * 3000 ) );
                  sharedLocation.set( count );
               }
      
               // if sleeping thread interrupted, print stack trace
               catch ( InterruptedException exception ) {
                  exception.printStackTrace();
               }
            }
              
            String name = getName();
            SwingUtilities.invokeLater( new RunnableOutput( outputArea, "\n" + 
               name + " done producing.\n" + name + " terminated.\n" ) );
              
         } // end method run
      } // end class Producer
        

    4. Consumer.java
      // Fig. 16.13: Consumer.java
      // Consumer?? run method controls a thread that loops ten
      // times and reads a value from sharedLocation each time.
      import javax.swing.*;
      
      public class Consumer extends Thread {
         private Buffer sharedLocation; // reference to shared object
         private JTextArea outputArea;
          
         // constructor
         public Consumer( Buffer shared, JTextArea output )
         {
            super( "Consumer" );
            sharedLocation = shared;
            outputArea = output;
         }
          
         // read sharedLocation's value ten times and sum the values
         public void run()
         {
            int sum = 0;
      
            for ( int count = 1; count <= 10; count++ ) {
               
               // sleep 0 to 3 seconds, read value from Buffer and add to sum
               try {
                  Thread.sleep( ( int ) ( Math.random() * 3001 ) );    
                  sum += sharedLocation.get();
               }
      
               // if sleeping thread interrupted, print stack trace
               catch ( InterruptedException exception ) {
                  exception.printStackTrace();
               }
            }
              
            String name = getName();
            SwingUtilities.invokeLater( new RunnableOutput( outputArea, 
               "\nTotal " + name + " consumed: " + sum + ".\n" + 
               name + " terminated.\n ") );
              
         } // end method run
      } // end class Consumer
        

    5. CircularBufferTest.java
      // Fig. 16.15: CircularBufferTest.java
      // CircularBufferTest shows two threads manipulating a circular buffer.
      import java.awt.*;
      import java.awt.event.*;
      import javax.swing.*;
      
      // set up the producer and consumer threads and start them
      public class CircularBufferTest extends JFrame {
         JTextArea outputArea;
      
         // set up GUI
         public CircularBufferTest() {
            super( "Demonstrating Thread Synchronizaton" );
              
            outputArea = new JTextArea( 20,30 );
            outputArea.setFont( new Font( "Monospaced", Font.PLAIN, 12 ) );
            getContentPane().add( new JScrollPane( outputArea ) );
              
            setSize( 310, 500 );
            setVisible( true );
              
            // create shared object used by threads; we use a CircularBuffer
            // reference rather than a Buffer reference so we can invoke 
            // CircularBuffer method createStateOutput
            CircularBuffer sharedLocation = new CircularBuffer( outputArea );
      
            // display initial state of buffers in CircularBuffer
            SwingUtilities.invokeLater( new RunnableOutput( outputArea, 
               sharedLocation.createStateOutput() ) );
      
            // set up threads
            Producer producer = new Producer( sharedLocation, outputArea );
            Consumer consumer = new Consumer( sharedLocation, outputArea );
              
            producer.start();  // start producer thread
            consumer.start();  // start consumer thread
      
         } // end constructor
          
         public static void main ( String args[] ) {
            CircularBufferTest application = new CircularBufferTest();
            application.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
         }
      } // end class CirclularBufferTest
        
  7. RandomCharacters.java

    There are several issues worth mentioning for the following example. First, there are two inner classes -- one controls threads created in the applet and one uses SwingUtilities.invokeLater() to ensure the GUI is updated properly.

    As we noted earlier, although Thread provides suspend(), stop(), and resume(), they are deprecated because suspend() may cause deadlock and stop() may cause data corruption. In this example, it shows how to suspend, stop, and resume threads using mechanisms rely on synchronized statement, loops, and boolean flag variables.

    // Fig. 16.16: RandomCharacters.java
    // Class RandomCharacters demonstrates the Runnable interface
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    
    public class RandomCharacters extends JApplet implements ActionListener {
       private String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
       private final static int SIZE = 3;
       private JLabel outputs[];
       private JCheckBox checkboxes[];   
       private Thread threads[];
       private boolean suspended[];
    
       // set up GUI and arrays
       public void init() {
          outputs = new JLabel[ SIZE ];
          checkboxes = new JCheckBox[ SIZE ];
          threads = new Thread[ SIZE ];
          suspended = new boolean[ SIZE ];
    
          Container container = getContentPane();
          container.setLayout( new GridLayout( SIZE, 2, 5, 5 ) );
    
          // create GUI components, register listeners and attach 
          // components to content pane
          for ( int count = 0; count < SIZE; count++ ) {
             outputs[ count ] = new JLabel();
             outputs[ count ].setBackground( Color.GREEN );
             outputs[ count ].setOpaque( true );
             container.add( outputs[ count ] );
    
             checkboxes[ count ] = new JCheckBox( "Suspended" );
             checkboxes[ count ].addActionListener( this );
             container.add( checkboxes[ count ] );
          }
    
       } // end method init
      
       // create and start threads each time start is called (i.e., after 
       // init and when user revists Web page containing this applet)
       public void start() {
          for ( int count = 0; count < threads.length; count++ ) {
    
             // create Thread; initialize object that implements Runnable
             threads[ count ] = 
                new Thread( new RunnableObject(), "Thread " + ( count + 1 ) );
    
             threads[ count ].start(); // begin executing Thread
          }
       }
    
       // determine thread location in threads array
       private int getIndex( Thread current ) {
          for ( int count = 0; count < threads.length; count++ )
             if ( current == threads[ count ] )
                return count;
    
          return -1; 
       }
    
       // called when user switches Web pages; stops all threads
       public synchronized void stop() {
          // set references to null to terminate each thread's run method
          for ( int count = 0; count < threads.length; count++ ) 
             threads[ count ] = null;
          
          notifyAll(); // notify all waiting threads, so they can terminate
       }
    
       // handle button events
       public synchronized void actionPerformed( ActionEvent event ) {
          for ( int count = 0; count < checkboxes.length; count++ ) {
    
             if ( event.getSource() == checkboxes[ count ] ) {
                suspended[ count ] = !suspended[ count ];
    
                // change label color on suspend/resume
                outputs[ count ].setBackground(
                   suspended[ count ] ? Color.RED : Color.GREEN );
    
                // if thread resumed, make sure it starts executing
                if ( !suspended[ count ] )
                   notifyAll(); 
    
                return;
             }
          }
       } // end method actionPerformed
    
       // private inner class that implements Runnable to control threads
       private class RunnableObject implements Runnable {
       
          // place random characters in GUI, variables currentThread and 
          // index are final so can be used in an anonymous inner class
          public void run() {
             // get reference to executing thread
             final Thread currentThread = Thread.currentThread();
    
             // determine thread's position in array
             final int index = getIndex( currentThread );
    
             // loop condition determines when thread should stop; loop 
             // terminates when reference threads[ index ] becomes null
             while ( threads[ index ] == currentThread ) {
                // sleep from 0 to 1 second
                try {
                   Thread.sleep( ( int ) ( Math.random() * 1000 ) );
    
                   // determine whether thread should suspend execution;
                   // synchronize on RandomCharacters applet object
                   synchronized( RandomCharacters.this ) {
    
                      while ( suspended[ index ] && 
                         threads[ index ] == currentThread ) {
    
                         // temporarily suspend thread execution
                         RandomCharacters.this.wait();  
                      }
                   } // end synchronized block
                } // end try
    
                // if thread interrupted during wait/sleep, print stack trace
                catch ( InterruptedException exception ) {
                   exception.printStackTrace();
                }
                
                // display character on corresponding JLabel
                SwingUtilities.invokeLater( 
                   new Runnable() {
                      
                      // pick random character and display it
                      public void run() {
                         char displayChar = 
                            alphabet.charAt( ( int ) ( Math.random() * 26 ) );
    
                         outputs[ index ].setText( 
                            currentThread.getName()  + ": " + displayChar );
                      }
                   } // end inner class
                ); // end call to SwingUtilities.invokeLater
             } // end while
    
             System.err.println( currentThread.getName() + " terminating" );
    
          } // end method run
       } // end private inner class RunnableObject
    } // end class RandomCharacters
      

Interthread Communications

There are three ways (as far as I know so far) for interthread communications and they are (1) communications via shared objects, (2) communications via communication pipes, and (3) the wait()/notify() methods. Using shared objects for coomunications were discussed in the previous section.

  1. Communication Pipes (java.io.PipedOutputStream and java.io.PipedInputStream)
    import java.io.*;
    
    public class PipeDemo extends Thread {
      PipedOutputStream output;
    
      // Create an instance of the PipeDemo class
      public PipeDemo(PipedOutputStream out) {
        // Copy to local member variable
        output = out;
      }
    
      public static void main (String args[]) {
        try {
          // Create a pipe for writing
          PipedOutputStream pout = new PipedOutputStream();
    
          // Create a new pipe demo thread, to write to our pipe
          PipeDemo pipedemo = new PipeDemo(pout);
    
          // One communication pipe is attached to a inputstream and a
          // an outputstream at both ends; respectively.
          //
          // Creates a piped input stream connected to the specified piped 
          // output stream. Data bytes written to pout will then be available 
          // as input from pin.
          PipedInputStream pin = new PipedInputStream(pout);
    
          // Start the thread
          pipedemo.start();
    
          // Read thread data,
          int input = pin.read();
    
          // Terminate when end of stream reached
          while (input != -1) {
            // Print message
            System.out.print ( (char) input);
    
            // Read next byte
            input = pin.read();
          }
        } catch (Exception e) {
          System.err.println ("Pipe error " + e);
        }
      }
    
      public void run() {
        try {
          // Create a printstream for convenient writing
          PrintStream p = new PrintStream( output );
    
          // Print message
          p.println ("Hello from another thread, via pipes!");
    
          // Close the stream
          p.close();
        } catch (Exception e) {
          // no code req'd
        }
      }
    }
    

  2. PipeDemo2.java
    import java.io.*;
    
    class PipeThread extends Thread {
      PipedOutputStream output;
      PipedInputStream input;
    
      public PipeThread(String s, PipedOutputStream out, PipedInputStream in) {
        super(s);
        output = out;
        input = in;
      }
    
      public void run() {
        PrintStream p;
        BufferedReader r;
    
        try {
          p = new PrintStream(output);
          r = new BufferedReader(new InputStreamReader(input));
    
          // Print message
          p.println ("你好 from " + getName());
          System.out.println(getName() + " received: " + r.readLine());
    
        } catch (Exception e) {
          // no code req'd
        }
      }
    }
    
    public class PipeDemo2 {
      public static void main (String args[]) {
        try {
          // create two communication pipes
          // NOTE that the pipe (including both PipedOutputStream
          // and PipedInputStream) must be constructed before the thread is started
          PipedOutputStream pout1 = new PipedOutputStream();
          PipedInputStream pin1 = new PipedInputStream(pout1);
          PipedOutputStream pout2 = new PipedOutputStream();
          PipedInputStream pin2 = new PipedInputStream(pout2);
    
          // Create a new pipe demo thread, to write to our pipe
          PipeThread pipedemo1 = new PipeThread("Thread 1", pout1, pin2);
          PipeThread pipedemo2 = new PipeThread("Thread 2", pout2, pin1);
    
          // Start the thread
          pipedemo1.start(); pipedemo2.start();
    
        } catch (Exception e) {}
      }
    }
    
    練習題:
    1. 請改寫這個範例使得 Thread 1, Thread 2, and main Thread 可以互通訊息。
    2. 請改寫從 1 加到 100,000 的範例,使其每一個 thread 的結果能夠互相交換 而得到總合。

Thread Groups

One of the advantages of thread groups is that it allows developers to group threads together and apply an operation on the group rather than on the individual elements of the group. A thread group may be composed of threads and other thread subgroups. Also, a thread or a thread subgroup can be added dynamically to a thread group at any point during a program's execution. However, there is no way to unhook a thread or a thread subgroup from a thread group.

Operations such as stopping or suspending may be performed on the subgroups or the parent group. When an operation is applied to the parent group, it will cause the operation to be propagated to every subgroups, which in turn will pass the operation on to every thread within that subgroup.

// Chapter 7, Listing 10
public class GroupDemo implements Runnable {
  public static void main(String args[]) throws Exception {
    // Create a thread group
    ThreadGroup parent = new ThreadGroup("parent");

    // Create a group that is a child of another thread group
    ThreadGroup subgroup = new ThreadGroup(parent, "subgroup");

    // Create some threads in the parent, and subgroup class
    Thread t1 = new Thread ( parent, new GroupDemo() );
    t1.start();
    Thread t2 = new Thread ( parent, new GroupDemo() );
    t2.start();
    Thread t3 = new Thread ( subgroup, new GroupDemo() );
    t3.start();

    // Dump the contents of the group to System.out
    parent.list();

    // Wait for user, then terminate
    System.out.println ("Press enter to continue");
    System.in.read();
    System.exit(0);
  }

  public void run() {
    // Do nothing
    for(;;) {
      Thread.yield();
    }
  }
}


Last Updated:
Written by: 國立中興大學資管系呂瑞麟 Eric Jui-Lin Lu