Multithreading with GUI
This program uses separate threads to modify the content displayed in a Swing GUI. The nature of multithreaded programming prevents the programmer from knowing exactly when a thread will execute. Swing components are not thread safeif multiple threads manipulate a Swing GUI component, the results may not be correct. All interactions with Swing GUI components should be performed as part of the event-dispatching thread (also known as the event-handling thread). Class SwingUtilities (package javax.swing) provides static method invokeLater to help with this process. Method invokeLater specifies GUI processing statements to execute later as part of the event-dispatching thread. Method invokeLater receives as its argument an object that implements interface Runnable. The method places the Runnable as an event into the event-dispatching thread's queue of events. These events are processed in the order they appear in the queue. Because only one thread handles these events, it can be guaranteed that the GUI will be updated properly.
Our next example (Fig. 23.17) demonstrates this concept. When this program calls invokeLater, the GUI component update will be queued for execution in the event-dispatching thread. The Runnable's run method will then be invoked as part of the event-dispatching thread to perform the output and ensure that the GUI component is updated in a thread-safe manner. This example also demonstrates how to suspend a thread (i.e., temporarily prevent it from executing) and how to resume a suspended thread.
Figure 23.17. RunnableObject outputs a random uppercase letter on a JLabel. Allows user to suspend and resume execution.
(This item is displayed on pages 1088 - 1090 in the print version)
1 // Fig. 23.17: RunnableObject.java 2 // Runnable that writes a random character to a JLabel 3 import java.util.Random; 4 import java.util.concurrent.locks.Condition; 5 import java.util.concurrent.locks.Lock; 6 import javax.swing.JLabel; 7 import javax.swing.SwingUtilities; 8 import java.awt.Color; 9 10 public class RunnableObject implements Runnable 11 { 12 private static Random generator = new Random(); // for random letters 13 private Lock lockObject; // application lock; passed in to constructor 14 private Condition suspend; // used to suspend and resume thread 15 private boolean suspended = false; // true if thread suspended 16 private JLabel output; // JLabel for output 17 18 public RunnableObject( Lock theLock, JLabel label ) 19 { 20 lockObject = theLock; // store the Lock for the application 21 suspend = lockObject.newCondition(); // create new Condition 22 output = label; // store JLabel for outputting character 23 } // end RunnableObject constructor 24 25 // place random characters in GUI 26 public void run() 27 { 28 // get name of executing thread 29 final String threadName = Thread.currentThread().getName(); 30 31 while ( true ) // infinite loop; will be terminated from outside 32 { 33 try 34 { 35 // sleep for up to 1 second 36 Thread.sleep( generator.nextInt( 1000 ) ); 37 38 lockObject.lock(); // obtain the lock 39 try 40 { 41 while ( suspended ) // loop until not suspended 42 { 43 suspend.await(); // suspend thread execution 44 } // end while 45 } // end try 46 finally 47 { 48 lockObject.unlock(); // unlock the lock 49 } // end finally 50 } // end try 51 // if thread interrupted during wait/sleep 52 catch ( InterruptedException exception ) 53 { 54 exception.printStackTrace(); // print stack trace 55 } // end catch 56 57 // display character on corresponding JLabel 58 SwingUtilities.invokeLater( 59 new Runnable() 60 { 61 // pick random character and display it 62 public void run() 63 { 64 // select random uppercase letter 65 char displayChar = 66 ( char ) ( generator.nextInt( 26 ) + 65 ); 67 68 // output character in JLabel 69 output.setText( threadName + ": " + displayChar ); 70 } // end method run 71 } // end inner class 72 ); // end call to SwingUtilities.invokeLater 73 } // end while 74 } // end method run 75 76 // change the suspended/running state 77 public void toggle() 78 { 79 suspended = !suspended; // toggle boolean controlling state 80 81 // change label color on suspend/resume 82 output.setBackground( suspended ? Color.RED : Color.GREEN ); 83 84 lockObject.lock(); // obtain lock 85 try 86 { 87 if ( !suspended ) // if thread resumed 88 { 89 suspend.signal(); // resume thread 90 } // end if 91 } // end try 92 finally 93 { 94 lockObject.unlock(); // release lock 95 } // end finally 96 } // end method toggle 97 } // end class RunnableObject |
Class RunnableObject (Fig. 23.17) implements interface Runnable's run method (lines 2674). Line 29 uses static Thread method currentThread to determine the currently executing thread and Thread method getName to return the name of that thread. Every executing thread has a default name that includes the number of the thread (see the output of Fig. 23.18). Lines 3173 are an infinite loop. [Note: In earlier chapters we have said that infinite loops are bad programming because the application will not terminate. In this case, the infinite loop is in a separate thread from the main thread. When the application window is closed in this example, all the threads created by the main thread are closed as well, including threads (such as this one) that are executing infinite loops.] In each iteration of the loop, the thread sleeps for a random interval from 0 to 1 seconds (line 36).
Figure 23.18. RandomCharacters creates a JFrame with three RunnableObjects and three JCheckBoxes to allow the user to suspend and resume threads.
(This item is displayed on pages 1091 - 1093 in the print version)
1 // Fig. 23.18: RandomCharacters.java 2 // Class RandomCharacters demonstrates the Runnable interface 3 import java.awt.Color; 4 import java.awt.GridLayout; 5 import java.awt.event.ActionEvent; 6 import java.awt.event.ActionListener; 7 import java.util.concurrent.Executors; 8 import java.util.concurrent.ExecutorService; 9 import java.util.concurrent.locks.Condition; 10 import java.util.concurrent.locks.Lock; 11 import java.util.concurrent.locks.ReentrantLock; 12 import javax.swing.JCheckBox; 13 import javax.swing.JFrame; 14 import javax.swing.JLabel; 15 16 public class RandomCharacters extends JFrame implements ActionListener 17 { 18 private final static int SIZE = 3; // number of threads 19 private JCheckBox checkboxes[]; // array of JCheckBoxes 20 private Lock lockObject = new ReentrantLock( true ); // single lock 21 22 // array of RunnableObjects to display random characters 23 private RunnableObject[] randomCharacters = 24 new RunnableObject[ SIZE ]; 25 26 // set up GUI and arrays 27 public RandomCharacters() 28 { 29 checkboxes = new JCheckBox[ SIZE ]; // allocate space for array 30 setLayout( new GridLayout( SIZE, 2, 5, 5 ) ); // set layout 31 32 // create new thread pool with SIZE threads 33 ExecutorService runner = Executors.newFixedThreadPool( SIZE ); 34 35 // loop SIZE times 36 for ( int count = 0; count < SIZE; count++ ) 37 { 38 JLabel outputJLabel = new JLabel(); // create JLabel 39 outputJLabel.setBackground( Color.GREEN ); // set color 40 outputJLabel.setOpaque( true ); // set JLabel to be opaque 41 add( outputJLabel ); // add JLabel to JFrame 42 43 // create JCheckBox to control suspend/resume state 44 checkboxes[ count ] = new JCheckBox( "Suspended" ); 45 46 // add listener which executes when JCheckBox is clicked 47 checkboxes[ count ].addActionListener( this ); 48 add( checkboxes[ count ] ); // add JCheckBox to JFrame 49 50 // create a new RunnableObject 51 randomCharacters[ count ] = 52 new RunnableObject( lockObject, outputJLabel ); 53 54 // execute RunnableObject 55 runner.execute( randomCharacters[ count ] ); 56 } // end for 57 58 setSize( 275, 90 ); // set size of window 59 setVisible( true ); // show window 60 61 runner.shutdown(); // shutdown runner when threads finish 62 } // end RandomCharacters constructor 63 64 // handle JCheckBox events 65 public void actionPerformed( ActionEvent event ) 66 { 67 // loop over all JCheckBoxes in array 68 for ( int count = 0; count < checkboxes.length; count++ ) 69 { 70 // check if this JCheckBox was source of event 71 if ( event.getSource() == checkboxes[ count ] ) 72 randomCharacters[ count ].toggle(); // toggle state 73 } // end for 74 } // end method actionPerformed 75 76 public static void main( String args[] ) 77 { 78 // create new RandomCharacters object 79 RandomCharacters application = new RandomCharacters(); 80 81 // set application to end when window is closed 82 application.setDefaultCloseOperation( EXIT_ON_CLOSE ); 83 } // end main 84 } // end class RandomCharacters
|
When the thread awakens, line 38 acquires the Lock on this application. Lines 4144 loop while the boolean variable suspended is TRue. Line 43 calls method await on Condition suspend to temporarily release the Lock and place this thread into the waiting state. When this thread is signaled, it reacquires the Lock, moves back to the runnable state and releases the Lock (line 48). When suspended is false, the thread should resume execution. If suspended is still true, the loop executes again.
Lines 5872 call SwingUtilities method invokeLater. Lines 5971 declare an anonymous inner class that implements the Runnable interface, and lines 6270 declare method run. The method call to invokeLater places this Runnable object in a queue to be executed by the event-dispatching thread. Lines 6566 create a random uppercase character. Line 69 calls method setText on the JLabel output to display the thread name and the random character on the JLabel in the application window.
When the user clicks the JCheckBox to the right of a particular JLabel, the corresponding thread should be suspended (temporarily prevented from executing) or resumed (allowed to continue executing). Suspending and resuming of a thread can be implemented by using thread synchronization and Condition methods await and signal. Lines 7796 declare method toggle, which will change the suspended/resumed state of the current thread. Line 79 reverses the value of boolean variable suspended. Line 82 changes the background color of the JLabel by calling method setBackground. If the thread is suspended, the background color will be Color.RED, whereas if the thread is running, the background color will be Color.GREEN. Method toggle is called from the event handler in Fig. 23.18, so its tasks will be performed in the event-dispatch threadthus, there is no need to use invokeLater for line 82. Line 84 acquires the Lock for this application. Line 87 then tests whether the thread was just resumed. If this is true, line 89 calls method signal on Condition suspend. This method call will alert a thread that was placed in the waiting state by the await method call in line 43. Line 94 releases the Lock on this application inside a finally block.
Note that the if statement in line 87 does not have an associated else. If this condition fails, it means that the thread has just been suspended. When this happens, a thread executing at line 38 will enter the while loop and line 43 will suspend the thread with a call to method await.
Class RandomCharacters (Fig. 23.18) displays three JLabels and three JCheckBoxes. A separate thread of execution is associated with each JLabel and JCheckBox pair. Each thread randomly displays letters from the alphabet in its corresponding JLabel object. Line 33 creates a new ExecutorService with method newFixedThreadPool. Lines 3656 iterate three times. Lines 3841 create and customize the JLabel. Line 44 creates the JCheckBox, and line 47 adds an ActionListener for each JCheckBox (this will be discussed later.) Lines 5152 create a new RunnableObject implements the Runnable interface. Line 55 executes the RunnableObject with one of the threads of execution created in runner in line 33.
If the user clicks the Suspended checkbox next to a particular JLabel, the program invokes method actionPerformed (lines 6574) to determine which checkbox generated the event. Lines 6873 determine which checkbox generated the event. Line 71 checks whether the source of the event is the JCheckBox in the current index. If it is, line 72 calls method toggle (lines 7592 of Fig. 23.17) on that RunnableObject.