Site Search:

Chapter 9 GUI Applications

<Back

The Swing single-thread rule: Swing components and models should be created, modified, and queried only from the event-dispatching thread.

Consider a split-model design when a data model must be shared by more than one thread and implementing a thread-safe data model would be inadvisable because of blocking, consistency, or complexity reasons.


Single-threaded GUI frameworks achieve thread safety via thread confinement; all GUI objects, including visual components and data models, are accessed exclusively from the event thread. single-threaded GUI frameworks can bound action listener to visual component, that submits a long-running task to an Executor, which uses a thread pool worker thread to take the workload off from the event thread. Once that task finishes, the thread pool worker thread generates feedback to the GUI by submitting a task to event thread to update the GUI. In that way, the GUI is always responsive.

In fact Single-threaded GUI frameworks is a general pattern, node.js also use this idea to manage the threads.



import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;

import javax.swing.*;

public class ListenerExamples {
    private static ExecutorService exec = Executors.newCachedThreadPool();

    private final JButton colorButton = new JButton("Change color");
    private final Random random = new Random();
    
    public static void main(String[] args) {
        ListenerExamples le = new ListenerExamples();
        le.backgroundRandom();
        le.longRunningTask();
        le.longRunningTaskWithFeedback();
        le.taskWithCancellation();
        le.runInBackground();
        
        JFrame frame = new JFrame("Chap9Demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //Create and set up the content pane.
        JPanel newContentPane = new JPanel(new BorderLayout());
        JTabbedPane tabbedPane = new JTabbedPane();
        newContentPane.add(tabbedPane, BorderLayout.CENTER);
        
        addButtons(tabbedPane, "colorButton", le.colorButton);
        addButtons(tabbedPane, "computeButton", le.computeButton);
        addButtons(tabbedPane, "Do-idle", le.button, le.label);
        addButtons(tabbedPane, "start-cancel", le.startButton, le.cancelButton);
        addButtons(tabbedPane, "start-cancel2", le.startButton2, le.cancelButton2, le.label2);
        newContentPane.setOpaque(true);
        frame.setContentPane(newContentPane);
        frame.pack();
        frame.setVisible(true);
    }
    
    private static void addButtons(JTabbedPane tabbedPane, String title, JComponent... comp) {
        JPanel pane = new JPanel();
        pane.setBorder(BorderFactory.createTitledBorder(title));
        pane.setLayout(new BoxLayout(pane, BoxLayout.X_AXIS));
        Dimension size = new Dimension(500, 100);
        for(JComponent jc : comp) {
            pane.setMaximumSize(size);
            pane.setPreferredSize(size);
            pane.setMinimumSize(size);
            pane.add(jc);
        }
        tabbedPane.addTab(title, pane);
    }

    private void backgroundRandom() {
        colorButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                colorButton.setBackground(new Color(random.nextInt()));
                colorButton.setOpaque(true);
            }
        });
    }


    private final JButton computeButton = new JButton("Big computation");

    private void longRunningTask() {
        computeButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                exec.execute(new Runnable() {
                    public void run() {
                        /* Do big computation */
                        try {
                            System.out.println("start big computation in longRunningTask");
                            Thread.sleep(5000);
                            System.out.println("DID...sl...err...a big...computation");
                            System.out.println("exiting longRunningTask");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        });
    }


    private final JButton button = new JButton("Do");
    private final JLabel label = new JLabel("idle");

    private void longRunningTaskWithFeedback() {
        button.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                button.setEnabled(false);
                label.setText("busy");
                exec.execute(new Runnable() {
                    public void run() {
                        try {
                            /* Do big computation */
                            System.out.println("start big computation in longRunningTaskWithFeedback");
                            Thread.sleep(6000);
                            System.out.println("exiting longRunningTaskWithFeedback");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            GuiExecutor.instance().execute(new Runnable() {
                                public void run() {
                                    button.setEnabled(true);
                                    label.setText("idle");
                                }
                            });
                        }
                    }
                });
            }
        });
    }

    private final JButton startButton = new JButton("Start");
    private final JButton cancelButton = new JButton("Cancel");
    private Future<?> runningTask = null; // thread-confined
    private volatile boolean moreWork = true;

    private void taskWithCancellation() {
        startButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (runningTask == null) {
                    runningTask = exec.submit(new Runnable() {
                        public void run() {
                            while (moreWork()) {
                                if (Thread.currentThread().isInterrupted()) {
                                    cleanUpPartialWork();
                                    break;
                                }
                                doSomeWork();
                            }
                            moreWork = true;
                        }

                        private boolean moreWork() {
                            return moreWork;
                        }

                        private void cleanUpPartialWork() {
                            System.out.println("cleanUpPartialWork and exit");
                            runningTask = null;
                        }

                        private void doSomeWork() {
                            try {
                                System.out.println("start doSomeWork() in taskWithCancellation");
                                Thread.sleep(5000);
                                System.out.println("end doSomeWork() in taskWithCancellation");
                                moreWork = false;
                                runningTask = null;
                                
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                                Thread.currentThread().interrupt();
                            }
                        }

                    });
                }
                ;
            }
        });

        cancelButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {
                if (runningTask != null)
                    runningTask.cancel(true);
            }
        });
    }


    private final JButton startButton2 = new JButton("Start");
    private final JButton cancelButton2 = new JButton("Cancel");
    private final JLabel label2 = new JLabel("idle");
    private volatile boolean moreWork2 = true;
    private void runInBackground() {
        startButton2.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                class CancelListener implements ActionListener {
                    BackgroundTask<?> task;
                    public void actionPerformed(ActionEvent event) {
                        if (task != null)
                            task.cancel(true);
                    }
                }
                final CancelListener listener = new CancelListener();
                moreWork2 = true;
                listener.task = new BackgroundTask<Void>() {
                    public Void compute() {
                        while (moreWork() && !isCancelled())
                            doSomeWork();
                        return null;
                    }

                    private boolean moreWork() {
                        return moreWork2;
                    }

                    private void doSomeWork() {
                        try {
                            this.setProgress(0, 100);
                            System.out.println("start running doSomeWork() in runInBackground");
                            Thread.sleep(2000);
                            this.setProgress(25, 100);
                            Thread.sleep(2000);
                            this.setProgress(50, 100);
                            Thread.sleep(2000);
                            this.setProgress(75, 100);
                            Thread.sleep(2000);
                            System.out.println("end running doSomeWork() in runInBackground");
                            moreWork2 = false;
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            Thread.currentThread().interrupt();
                        }
                    }
                    
                    protected void onProgress(int current, int max) {
                        label2.setText(current*100/max + " percent finished");
                    }

                    public void onCompletion(Void result, Throwable exception, boolean cancelled) {
                        cancelButton2.removeActionListener(listener);
                        label2.setText("done");
                    }
                };
                cancelButton2.addActionListener(listener);
                exec.execute(listener.task);
            }
        });
    }
}

abstract class BackgroundTask <V> implements Runnable, Future<V> {
    private final FutureTask<V> computation = new Computation();

    private class Computation extends FutureTask<V> {
        public Computation() {
            super(new Callable<V>() {
                public V call() throws Exception {
                    return BackgroundTask.this.compute();
                }
            });
        }

        protected final void done() {
            GuiExecutor.instance().execute(new Runnable() {
                public void run() {
                    V value = null;
                    Throwable thrown = null;
                    boolean cancelled = false;
                    try {
                        value = get();
                    } catch (ExecutionException e) {
                        thrown = e.getCause();
                    } catch (CancellationException e) {
                        cancelled = true;
                    } catch (InterruptedException consumed) {
                    } finally {
                        onCompletion(value, thrown, cancelled);
                    }
                };
            });
        }
    }

    protected void setProgress(final int current, final int max) {
        GuiExecutor.instance().execute(new Runnable() {
            public void run() {
                onProgress(current, max);
            }
        });
    }

    // Called in the background thread
    protected abstract V compute() throws Exception;

    // Called in the event thread
    protected void onCompletion(V result, Throwable exception,
                                boolean cancelled) {
    }

    protected void onProgress(int current, int max) {
    }

    // Other Future methods just forwarded to computation
    public boolean cancel(boolean mayInterruptIfRunning) {
        return computation.cancel(mayInterruptIfRunning);
    }

    public V get() throws InterruptedException, ExecutionException {
        return computation.get();
    }

    public V get(long timeout, TimeUnit unit)
            throws InterruptedException,
            ExecutionException,
            TimeoutException {
        return computation.get(timeout, unit);
    }

    public boolean isCancelled() {
        return computation.isCancelled();
    }

    public boolean isDone() {
        return computation.isDone();
    }

    public void run() {
        computation.run();
    }
}

class GuiExecutor extends AbstractExecutorService {
    // Singletons have a private constructor and a public factory
    private static final GuiExecutor instance = new GuiExecutor();

    private GuiExecutor() {
    }

    public static GuiExecutor instance() {
        return instance;
    }

    public void execute(Runnable r) {
        if (SwingUtilities.isEventDispatchThread())
            r.run();
        else
            SwingUtilities.invokeLater(r);
    }

    public void shutdown() {
        throw new UnsupportedOperationException();
    }

    public List<Runnable> shutdownNow() {
        throw new UnsupportedOperationException();
    }

    public boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException {
        throw new UnsupportedOperationException();
    }

    public boolean isShutdown() {
        return false;
    }

    public boolean isTerminated() {
        return false;
    }

}


In the above example,

  1. ColorButton tab demonstrated a simple event listener with the entire control flow confined in the event thread.
  2. ComputeButton tab demonstrated an action listener bound to a button, that submits a long-running task to an Executor. This longing running task runs in another thread so that the event thread is not occupied, therefore the GUI remains responsive while the task runs.
  3. Do-idle tab demonstrated an action listener bound to "Do" button, that submits a long-running task to an Executor. At the end of the task execution, the Executor submit a GUI updating task to run in the event thread, which gives feedback to the user interface to reflect the update. Helper class GuiExecutor built atop SwingUtilities make sure all the GUI update actions are confined in the event thread. We should always update GUI through GuiExecutor.
  4. Start-Cancel tab demonstrated an action listener bound to "Start" button, that submits a long-running task to an Executor. The Executor returned Future object is used to cancel the long-running task in an action listener bound to "Cancel" button, that calls Future.cancel.
  5. Start-Cancel2 tab demonstrated an action listener bound to "start" button, that initiates a long-running, cancellable task with BackgroundTask. BackgroundTask class has a FutureTask that updates the GUI in done() method through GuiExecutor class. It also supports cancellation, progress notification. It should notice that we should call onProgress() method to update the GUI via GuiExecutor instead of directly updating the GUI components in the doWork(). doWork() is running outside the Event thread, it is the wrong place to update GUI.