Event Dispatch Thread
In a Swing application, most of the processing takes place in a single, special thread called the event dispatch thread.
This thread becomes active after a component becomes realized : either pack, show, or setVisible(true) has been called. When a top level window is realized, all of its components are also realized. Swing is mostly single-threaded : almost all calls to realized components should execute in the event dispatch thread. The thread-safe exceptions are
- some methods of JComponent : repaint, revalidate, invalidate
- all addXXXListener and removeXXXListener methods
- all methods explicitly documented as thread-safe
If a task needs a relatively long time to complete, then performing that task in the event dispatch thread will cause the user interface to become unresponsive for the duration of the task - the GUI becomes "locked". Since this is undesirable, such tasks are usually performed outside the event dispatch thread, on what is commonly referred to as a worker thread.
When a worker thread completes its task, it needs a special mechanism for updating realized GUI components, since, as stated above, realized components almost always need to be updated only from the event dispatch thread. Two methods of the EventQueue class, invokeLater and invokeAndWait, are provided for this purpose (SwingUtilities has synonymous methods as well). Sun recommends using invokeLater as the usual preferred style.
When using threads, it is usually a good idea to use daemon threads, not user threads, whenever possible : daemon threads will not prevent an application from terminating. Since threads are user threads by default, an explicit call to Thread.setDaemon(true) is required.
The SwingWorker class was introduced in JSE 6, and should be used if available. The following examples use JSE 1.5.
Example 1
The Splash Screen topic (and in particular its Launcher class) is a good example of using a worker thread. Here, the status of the launch thread as a worker thread is exploited to show a splash screen to the user, but only until the main window has finished loading.
Example 2
The ColorTip class, shown below, changes the background color of a component for a short, fixed interval of time, as a simple way of calling attention to that component.
Its worker thread does not work very hard - it sleeps a lot. The calls to sleep do not cause the GUI to become unresponsive, however, since these calls do not take place in the event dispatch thread.
ColorTip has three private, Runnable nested classes :
- Worker - inserts specific time intervals between changing colors
- ChangeColor - updates the GUI by changing the background color of a target component
- RevertColor - updates the GUI by changing the background color back to its original color
package hirondelle.stocks.quotes; import java.util.logging.*; import java.awt.*; import javax.swing.*; import hirondelle.stocks.util.Consts; import hirondelle.stocks.util.Args; import hirondelle.stocks.util.Util; /** * Calls user's attention to an aspect of the GUI (much like a * <tt>ToolTip</tt>) by changing the background color of a * component (typically a <tt>JLabel</tt>) for a few seconds; * the component will always revert to its original background color * after a short time has passed. * * <p>Example use case: <pre> //no initial delay, and show the new color for 2 seconds only ColorTip tip = new ColorTip(0, 2, someLabel, temporaryColor); tip.start(); </pre> * * Uses a daemon thread, so this class will not prevent a program from * terminating. Will not lock the GUI. */ final class ColorTip { /** * Constructor. * * @param aInitialDelay number of seconds to wait before changing the * background color of <tt>aComponent</tt>, and must be in range 0..60 (inclusive). * @param aActivationInterval number of seconds to display <tt>aTempColor</tt>, * and must be in range 1..60 (inclusive). * @param aComponent GUI item whose background color will be changed. * @param aTempColor background color which <tt>aComponent</tt> will take for * <tt>aActivationInterval</tt> seconds. */ ColorTip ( int aInitialDelay, int aActivationInterval, JComponent aComponent, Color aTempColor ) { Args.checkForRange(aInitialDelay, 0, Consts.SECONDS_PER_MINUTE); Args.checkForRange(aActivationInterval, 1, Consts.SECONDS_PER_MINUTE); Args.checkForNull(aTempColor); fInitialDelay = aInitialDelay; fActivationInterval = aActivationInterval; fComponent = aComponent; fTemporaryColor = aTempColor; fOriginalColor = aComponent.getBackground(); fOriginalOpacity = aComponent.isOpaque(); } /** * Temporarily change the background color of the component, without interfering with * the user's control of the gui, and without preventing program termination. * * <P>If the target temporary color is the same as the current background color, then * do nothing. (This condition occurs when two <tt>ColorTip</tt> objects are * altering the same item at nearly the same time, such that they "overlap".) */ void start(){ if ( isSameColor() ) return; Thread thread = new Thread( new Worker() ); thread.setDaemon(true); thread.start(); } // PRIVATE // private final int fInitialDelay; private final int fActivationInterval; private final JComponent fComponent; private final Color fTemporaryColor; private final Color fOriginalColor; private final int fCONVERSION_FACTOR = Consts.MILLISECONDS_PER_SECOND; /** * Stores the original value of the opaque property of fComponent. * * Changes to the background color of a component * take effect only if the component is in charge of drawing its background. * This is defined by the opaque property, which needs to be true for these * changes to take effect. * * <P>If fComponent is not opaque, then this property is temporarily * changed by this class in order to change the background color. */ private final boolean fOriginalOpacity; private static final Logger fLogger = Util.getLogger(ColorTip.class); /** * Return true only if fTemporaryColor is the same as the fOriginalColor. */ private boolean isSameColor(){ return fTemporaryColor.equals(fOriginalColor); } private final class Worker implements Runnable { public void run(){ try { fLogger.fine("Initial Sleeping..."); Thread.sleep(fCONVERSION_FACTOR * fInitialDelay); EventQueue.invokeLater( new ChangeColor() ); fLogger.fine("Activation Sleeping..."); Thread.sleep(fCONVERSION_FACTOR * fActivationInterval); EventQueue.invokeLater( new RevertColor() ); } catch (InterruptedException ex) { fLogger.severe("Cannot sleep."); } fLogger.fine("Color worker done."); } } private final class ChangeColor implements Runnable { public void run(){ if ( ! fOriginalOpacity ) { fComponent.setOpaque(true); } fComponent.setBackground(fTemporaryColor); } } private final class RevertColor implements Runnable { public void run(){ fComponent.setBackground(fOriginalColor); fComponent.setOpaque(fOriginalOpacity); } } }
Example 3
The FetchQuotesAction class fetches current stock price data from a source on the web. Since this may take a few seconds to process, it uses a worker thread.
FetchQuotesAction uses two private, Runnable nested classes, in what is probably a typical style :
- HardWorker - performs an intensive task (here, fetches stock price data from the web)
- GuiUpdater - updates GUI components with the results of HardWorker (here, displays current stock price data)
package hirondelle.stocks.quotes; import java.awt.event.*; import java.awt.*; import javax.swing.*; import java.util.logging.*; import java.util.*; import java.text.MessageFormat; import hirondelle.stocks.util.Consts; import hirondelle.stocks.util.Util; import hirondelle.stocks.util.Args; import hirondelle.stocks.util.DataAccessException; import hirondelle.stocks.util.ui.UiConsts; import hirondelle.stocks.util.ui.UiUtil; import hirondelle.stocks.portfolio.CurrentPortfolio; import hirondelle.stocks.table.QuoteTable; import hirondelle.stocks.preferences.QuoteTablePreferencesEditor; /** * Fetch current quote data for the {@link CurrentPortfolio} from a data * source on the web. * * <P>This action is performed at many different times : *<ul> * <li>once upon startup * <li>periodically, at an interval configured by the user * <li>when the end user explicitly requests a refresh of quote info * <li>when the user makes a change to the current portfolio *</ul> * * <P>This class performs most of its work in a background daemon thread, and uses * <tt>EventQueue.invokeLater</tt> to update the user interface with the result. * The user interface remains responsive, regardless of the time taken for its * work to complete. * * <P>A daemon thread is used since daemon threads do not prevent an application * from terminating. */ public final class FetchQuotesAction extends AbstractAction implements Observer { /** * Constructor. * * @param aCurrentPortfolio an <tt>Observable</tt> which notifies this * object when the {@link CurrentPortfolio} is changed * @param aQuoteTablePrefEditor allows this class to read the user preference * for the frequency of periodic fetches * @param aQuoteTable a GUI element which is updated when a fetch is performed * @param aSummaryView a GUI element which is updated when a fetch is performed */ public FetchQuotesAction ( CurrentPortfolio aCurrentPortfolio, QuoteTablePreferencesEditor aQuoteTablePrefEditor, QuoteTable aQuoteTable, SummaryView aSummaryView ) { super("Update", UiUtil.getImageIcon("/toolbarButtonGraphics/general/Refresh")); Args.checkForNull(aQuoteTable); Args.checkForNull(aSummaryView); fCurrentPortfolio = aCurrentPortfolio; fQuoteTablePrefEditor = aQuoteTablePrefEditor; fQuoteTable = aQuoteTable; fSummaryView = aSummaryView; putValue(SHORT_DESCRIPTION, "Fetch updated stock quotes from web"); putValue( ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F5, UiConsts.NO_KEYSTROKE_MASK) ); putValue( LONG_DESCRIPTION, "Retrieves fresh stock quotes and displays it to the user in a table." ); putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_U) ); fUpdateFreq = fQuoteTablePrefEditor.getUpdateFrequency(); } /** * Start an internal timer. * <P>This method must be called immediately after calling the constructor. * Since this operation uses a 'this' reference, it cannot be included in the * constructor itself. */ public void startTimer(){ fQuoteTablePrefEditor.addObserver(this); fCurrentPortfolio.addObserver(this); fTimer = new javax.swing.Timer(fUpdateFreq * CONVERSION_FACTOR, this); fTimer.start(); } /** Fetch quotes from the web for the <tt>CurrentPortfolio</tt>. */ public void actionPerformed(ActionEvent e) { fLogger.info("Fetching quotes from web."); fSummaryView.showStatusMessage("Fetching quotes..."); Thread worker = new Thread(new HardWorker()); worker.setDaemon(true); worker.start(); } /** * Listens for changes to the <tt>CurrentPortfolio</tt> or the user * preference for update frequency, and calls {@link #actionPerformed}. * * <P>In the case of changes to the update frequency, <tt>actionPerformed</tt> is * called only if the udate frequency has been assigned a new value. */ public void update(Observable aPublisher, Object aData) { fLogger.fine("Notified ..."); if ( aPublisher == fQuoteTablePrefEditor ) { fLogger.fine("By StocksTablePrefEditor..."); boolean hasChangedFreq = fQuoteTablePrefEditor.getUpdateFrequency()!= fUpdateFreq; if ( hasChangedFreq ) restartTimer(); } else { fLogger.fine("By Current Portfolio..."); actionPerformed(null); } } // PRIVATE // private static final Logger fLogger = Util.getLogger(FetchQuotesAction.class); /** * The set of {@link Stock} objects in which the user * is currently interested. */ private CurrentPortfolio fCurrentPortfolio; /** * Edits user preferences attached to a table which presents quote data, and * allows read-only programmatic access to such preferences. */ private QuoteTablePreferencesEditor fQuoteTablePrefEditor; /** * GUI element which is updated whenever a new set of quotes is obtained. */ private QuoteTable fQuoteTable; /** * GUI element which is updated whenever a new set of quotes is obtained. */ private SummaryView fSummaryView; /** * Periodically fetches quote data. * * <P>Use of a Swing Timer is acceptable here, in spite of the fact that the task * takes a long time to complete, since the task does <em>not</em> in fact get * executed on the event-dispatch thread (See below.) */ private javax.swing.Timer fTimer; /** * The number of minutes to wait between fetches of quote information. */ private int fUpdateFreq; private static final int CONVERSION_FACTOR = Consts.MILLISECONDS_PER_SECOND * Consts.SECONDS_PER_MINUTE ; /** * Perform the fetch, and update the GUI elements using the event dispatch * thread. * * <P>Should be run as a daemon thread, such that it never prevents the application * from exiting. */ private final class HardWorker implements Runnable { public void run() { //simulateLongDelay(); try { java.util.List<Quote> quotes = fCurrentPortfolio.getPortfolio().getQuotes(); EventQueue.invokeLater( new GuiUpdater(quotes) ); } catch(DataAccessException ex) { EventQueue.invokeLater( new GuiUpdater(ex) ); } } } /** * Update the user interface after the fetch completes. * * <P>Must be run on the event dispatch thread. If the fetch fails for any reason, * then any current quote data is left as is, and an error message is displayed in * a status message. */ private final class GuiUpdater implements Runnable { GuiUpdater( java.util.List<Quote> aQuotes ){ fQuotes = aQuotes; } GuiUpdater(DataAccessException ex){ //the exception is not used in this implementation } public void run(){ if (fQuotes != null){ fQuoteTable.setQuoteTable( fQuotes ); fSummaryView.setQuotes( fQuotes ); StringBuilder warning = new StringBuilder(); if ( hasNoZeroPrices(warning) ){ fSummaryView.showStatusMessage("Done."); } else { fSummaryView.showStatusMessage(warning.toString()); } } else { fSummaryView.showStatusMessage("Failed - Please connect to the web."); } } private java.util.List<Quote> fQuotes; private MessageFormat fTickerWarningFormat = new MessageFormat("Warning - no price for ticker {0} ({1})") ; private boolean hasNoZeroPrices(StringBuilder aMessage){ for(Quote quote: fQuotes){ if ( Util.isZeroMoney(quote.getPrice()) ) { Object[] params = { quote.getStock().getTicker(), quote.getStock().getExchange() }; aMessage.append(fTickerWarningFormat.format(params)); return false; } } return true; } } /** Use for testing purposes only. */ private void simulateLongDelay(){ try { Thread.sleep(20000); } catch (InterruptedException ex) { System.out.println(ex); } } private void restartTimer(){ fLogger.fine("Resetting initial delay and delay to: " + fUpdateFreq + " minutes."); fUpdateFreq = fQuoteTablePrefEditor.getUpdateFrequency(); fTimer.setInitialDelay(fUpdateFreq * CONVERSION_FACTOR); fTimer.setDelay(fUpdateFreq * CONVERSION_FACTOR); fLogger.fine("Cancelling pending tasks, and restarting timer..."); fTimer.restart(); } }
|
|