Skip to main content

RepaintManager and Dirty Reads

Posted by kschaefe on January 19, 2011 at 7:01 AM PST

The first rule of Swing programming is to always interact with Swing components in the Event Dispatch Thread (EDT, for short).  Swing is single-threaded (as a lot of UI toolkits are) and as such it can only makes guarantees about the state of Swing components when interacted with properly on the EDT.  Some Swing methods are thread-safe, such as JComponent.repaint.  Recently, I discovered that RepaintManager, the class that handles Swing painting (used by JComponent.repaint), can access a JComponent's state off of the EDT.

The Problem

Here's what happened. My company, to ensure that we do not violate the Swing "prime directive," uses an aspect to interleave EDT checking code in our internal builds.  During one of our latest builds, we started seeing threading violations reported.  At first, we thought it was the new look and feel we were using doing something untoward, but it was simply calling JComponent.repaint() from an animation thread.  Digging into the internals of the RepaintManager, I found the following:

  1. JComponent.repaint calls RepaintManager.addDirtyRegion.
  2. RepaintManager.addDirtyRegion calls into the private implementation RepaintManager.addDirtyRegion0.
  3. RepaintManager.addDirtyRegion0 calls JComponent methods without ensuring the calls happen on the EDT.
    1. getWidth()
    2. getHeight()
    3. getParent()
    4. isVisible()

The following class demonstrates the "problem:" 

import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.*;

import javax.swing.*;

public class RepaintThreadViolations {
    private static class TestingComponent extends JLabel {
        public int getWidth() {
            assert SwingUtilities.isEventDispatchThread();
            return super.getWidth();
        }
       
        public int getHeight() {
            assert SwingUtilities.isEventDispatchThread();
            return super.getHeight();
        }
    }
   
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                final JLabel label = new TestingComponent();
                label.setText("Hello world!");
                frame.add(label);
                frame.add(new JButton(getAction(label)), BorderLayout.SOUTH);
                frame.pack();
                frame.setVisible(true);
            }
        });
    }
   
    private static Action getAction(final JComponent comp) {
        return new AbstractAction("Repaint") {
            public void actionPerformed(ActionEvent e) {
               
                SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
                    protected Void doInBackground() throws Exception {
                        comp.repaint();
                       
                        return null;
                    }
                   
                    protected void done() {
                        try {
                            get();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } catch (ExecutionException e) {
                            e.printStackTrace();
                        }
                    }
                };
                worker.execute();
            }
        };
    }
}
 

The embedded SwingWorker is calling the repaint on a background thread.  This will trip the assertions in TestingComponent.getWidth and TestingComponent.getHeight because RepaintManager is not calling the methods from the EDT.

Is it thread-safe?

Technically speaking, RepaintManager is not thread-safe.  Each call to addDirtyRegion0 makes several calls to Swing methods that are known to be not thread-safe.  As such, it is possible that RepaintManager may obtain stale data from a dirty read.  The following steps illustrate that possibility:

Stale Data Example for RepaintManager.addDirtyRegion0
Thread 1 (EDT) Thread 2
Existing Swing component has a size of 0x0.  
  Swing component is repainted with repaint().
 

RepaintManager.addDirtyRegion0 calls getWidth().

 Component receives a new size of 100x100.

 

 

RepaintManager believes that the component's size is 0x0.

 Component fields are updated with the new values.

 

Even though the component's size is in the process of updating to 100x100, the RepaintManager obtains 0x0.  Furthermore, if the a component returns a height or a width of 0, RepaintManager will not even queue it for painting!

How does it work?

RepaintManager is trading accuracy for speed.  Most of the time the value will not be stale and it is a lot faster to read (a possibly stale) value, than to synchronize (or to serialize the calls on the EDT).  So, how can RepaintManager get away with this?  The reason is simple: anything that would change the size of a component or its visibility actually occurs on the Event Dispatch Thread and those changes always cause a repaint to occur.  This means that in our example above once the component's size is set to 100x100, a new event will cause the RepaintManager to repaint the component.  That event will be on the EDT and the RepaintManager will have accurate information.  So even if the RepaintManager fails to repaint because of stale data an EDT-based event will cause another repaint to happen that all is well.  Furthermore, the actual painting occurs on the EDT, this gives the RepaintManager another bite at the apple where it can ensure that the data it has is accurate.

This does not mean that getHeight, getWidth, or isVisible are thread-safe; this is an advanced optimization based on a deep understanding of Swing and how it propagates events.  I would not recommend anyone to build Swing code in this way, but this overview gives a good understanding of how it is possible to break the Swing threading model and still come out on top.

I would like to thank my friend Alex for reviewing this blog.

Related Topics >>