Thread Watch

When off-loading a lengthy operation to a different thread, the preferred method of monitoring the thread's progress is to use a SwingWorker instance.

Yet another technique passes a continuation, as shown here. A similar approach is discussed in more detail by Goetz, et al. in chapter 9 of Java Concurrency in Practice. Correct synchronization in general is discussed in this video presentation Advanced Topics in Programming Languages: The Java Memory Model

With some care, the alternative approach below may be used. Having a simple MVC architecture, the Model evolves an offscreen image in a separate thread. The probability that a pixel will be modified in an iteration of the Model is given by a Random number having a Gaussian distribution. This produces the ripple effect seen below. A Swing Timer periodically notifies the View that new image data should be rendered. A Control instance allows the image to be reset and the update rate adjusted. The fill checkbox causes the Model to switch to a gray-scale palette. As the window is resized or the rate adjusted, it is easy to see the image being updated while it is also being scaled to fill the View.

package thread;

import java.awt.AlphaComposite;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.Timer;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

 * @author John B. Matthews
public class ThreadWatch {

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            public void run() {
                new ThreadWatch().create();

    private void create() {
        JFrame f = new JFrame("Thread Watch");
        Model model = new Model();
        View view = new View(model);
        Control control = new Control(model);
        JPanel panel = new JPanel(new BorderLayout());
        panel.add(view, BorderLayout.CENTER);
        panel.add(control, BorderLayout.SOUTH);
        new Thread(model).start();

class Control extends JPanel implements ActionListener, ChangeListener, ItemListener {

    private static final String RESET = "Reset";
    private final JCheckBox fillCheck = new JCheckBox("Fill");
    private Model model;

    public Control(Model model) {
        this.model = model;
        JLabel label = new JLabel("Rate (Hz):");
        JSpinner speed = new JSpinner(new SpinnerNumberModel(Model.RATE, 1, 50, 1));
        speed.setEditor(new JSpinner.NumberEditor(speed, "0"));

    private void addButton(String name) {
        JButton button = new JButton(name);

    public void actionPerformed(ActionEvent e) {
        String cmd = e.getActionCommand();
        if (RESET.equals(cmd)) {

    public void stateChanged(ChangeEvent e) {
        JSpinner spinner = (JSpinner) e.getSource();
        Number value = (Number) spinner.getValue();
        model.getTimer().setDelay(1000 / value.intValue());

    public void itemStateChanged(ItemEvent e) {
        Object source = e.getItemSelectable();
        boolean state = e.getStateChange() == ItemEvent.SELECTED;
        if (source == fillCheck) {

class View extends JPanel implements Observer {

    private static final int WIDE = 600;
    private static final int HIGH = WIDE * Model.ASPH / Model.ASPW;
    private Model model;

    public View(Model model) {
        this.model = model;
        this.setPreferredSize(new Dimension(WIDE, HIGH));

    protected void paintComponent(Graphics g) {
        ((Graphics2D) g).setComposite(AlphaComposite.Src);
        g.drawImage(model.getImage(), 0, 0,
            this.getWidth(), this.getHeight(), null);

    public void update(Observable o, Object arg) {

 * A stochastic model that simulates the accumulation of image data.
 * The model runs on a separate thread. A javax.swing.Timer is used for interim
 * updates so that the actionPerformed() method executes on the
 * event-dispatching thread. As this relies on the Observable's call to
 * update(), avoid calling notifyObservers() from any other thread. Note also
 * that image is final [JLS 17.5].
class Model extends Observable implements ActionListener, Runnable {

    public static final int RATE = 25; // 25 Hz
    public static final int ASPW = 5;  // Aspect ratio width
    public static final int ASPH = 3;  // Aspect ratio height
    private static final int WIDE = 256;
    private static final int HIGH = WIDE * ASPH / ASPW;
    private static final Random random = new Random();
    private static final Color[] clut = new Color[16];

    static {
        for (int i = 0; i < clut.length; i++) {
            int v = i * 16;
            clut[i] = new Color(v, v, v);
    private final Timer timer = new Timer(1000 / RATE, this);
    private final BufferedImage image = new BufferedImage(WIDE, HIGH, BufferedImage.TYPE_INT_ARGB);
    private final Graphics2D g2d = image.createGraphics();
    private final WritableRaster raster = image.getRaster();
    private final int[] ia = new int[4];
    private boolean fill;
    private int index;

    public void run() {
        while (true) {

    private void next() {
        if (fill) {
            if (index == clut.length) {
                index = 0;
            g2d.fillRect(0, 0, WIDE, HIGH);
        } else {
            double dx = Math.abs(random.nextGaussian());
            double dy = Math.abs(random.nextGaussian());
            int x = (int) (dx * WIDE / 2.123d);
            int y = (int) (dy * HIGH / 2.123d);
            if (x < WIDE && y < HIGH) {
                raster.getPixel(x, y, ia);
                raster.setPixel(x, y, ia);

    private void adjust(int i) {
        if (ia[i] > 255) {
            ia[i] = 128;

    public synchronized BufferedImage getImage() {
        return image;

    public synchronized Timer getTimer() {
        return timer;

    public synchronized void setFill(boolean fill) {
        this.fill = fill;

    public synchronized void reset() {
        g2d.fillRect(0, 0, WIDE, HIGH);
        Color c = Color.getHSBColor(random.nextFloat(), 0.9f, 0.9f);
        float[] fa = c.getRGBComponents(null);
        g2d.setPaint(new Color(fa[0], fa[1], fa[2], 0.75f));
        g2d.fillRect(0, 0, WIDE, HIGH);

    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == timer) {

Copyright © 2009 John B. Matthews. Distributed under the terms of the GPL