enzom83
enzom83

Reputation: 8310

Update java UI by different threads

I order to develop a swing application, I am using a simplified version of the MVC pattern, in which the controller is interposed between the view and the model in both directions:

A complex application may request different threads running: for example, if you must write a file on disk, you could start a background thread, and at the end of the writing of the file, this thread should send a notification to view via the controller. In this case, it may happen that more than one thread wants to refresh the view, so I assume that this issue should be handled in some way.

I took inspiration from this answer in order to write the methods appendText and updateLastText of the following SSCCE.

public class NewJFrame extends javax.swing.JFrame {

    private volatile String lastText;

    /**
     * Creates new form NewJFrame
     */
    public NewJFrame() {
        initComponents();
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        jScrollPane1 = new JScrollPane();
        jTextArea1 = new JTextArea();

        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        jTextArea1.setEditable(false);
        jTextArea1.setColumns(20);
        jTextArea1.setRows(5);
        jScrollPane1.setViewportView(jTextArea1);

        getContentPane().add(jScrollPane1, BorderLayout.CENTER);

        pack();
    }// </editor-fold>                        


    // Variables declaration - do not modify                     
    private JScrollPane jScrollPane1;
    private JTextArea jTextArea1;
    // End of variables declaration                   

    /**
     *
     * @param text
     */
    public void appendText(final String text) {
        updateLastText(text);
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                jTextArea1.append(String.format("%s\n", lastText));
            }
        });
    }

    /**
     *
     * @param text
     */
    private synchronized void updateLastText(String text) {
        lastText = text;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) {

        final NewJFrame frame = new NewJFrame();

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                frame.setVisible(true);
            }
        });

        Thread counter = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(NewJFrame.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    frame.appendText((new Date()).toString());
                }
            }
        };
        counter.start();
    }
}

The above solution seems to work properly, so should I use the same technique to update other swing component of the UI? Or is there a more compact solution?

Upvotes: 1

Views: 1559

Answers (2)

Marco13
Marco13

Reputation: 54639

Regarding the general question of whether you should use this method for all Swing component updates: Yes, you should do this.

Regarding the question of whether there is a more compact solution: None that I'm aware of. There are infrastructures for event handling in general (e.g. the Guava EventBus), but none of them releases one from the burden of carefully thinking about which thread is going to do what (and that all actions on Swing Components are done by the EDT, in particular).

These calls can only be made slightly "more compact" in the sense that the Runnable can often be written as a lambda in Java 8.


A side note: You should probably decide to either use the EventQueue or the SwingUtlities. I prefer the latter, but the invokeLater methods of these classes are eqivalent. It's just that using them alternatingly may be confusing.


Another side note: Sometimes it can be worthwhile to check whether the intented action already is about to be executed on the event dispatch thread. For example, you might consider writing one or the other method like this:

public void appendText(final String text) 
{
    executeOnEventDispatchThread(() -> textField.setText(text));
}

private static void executeOnEventDispatchThread(Runnable runnable)
{
    if (SwingUtilities.isEventDispatchThread())
    {
        runnable.run();
    }
    else
    {
        SwingUtilities.invokeLater(runnable);
    }
}

When appendText is called on the event dispatch thread, it will directly set the text in the text field. When it is called from a different thread, it will place the task to update the text on the event queue.

Edit in response to the comment

Sometimes it is "obvious" that a certain modification is done from the right thread. When you want to append text in the text field, for example, from the actionPerformed method of an ActionListener that is attached to a button, then this actionPerformed method will already be executed on the event dispatch thread - so there is no need to think about threading issues.

But sometimes you have the GUI directly or indirectly attached as a listener to some data model. And you don't know on which threads modifications on this data model take place. For example: Imagine a simple data model

class Model {

    void addModelListener(ModelListener listener) { ... }

    String getValue() { ... }

    void setValue(String newValue) {
        ....
        // Notify all ModelListeners here...
    }
}

Now, some GUI component is attached to this, in order to display the "value" in some label:

model.addModelListener( event -> label.setText(model.getValue()) );

This is perfectly fine as long as the model is only modified on the Event Dispatch Thread. For example:

someButton.addActionListener( event -> model.setValue("42") );

When the button is pressed, the ActionListener is executed on the Event Dispatch Thread. The call to setValue will in turn notify all ModelListener, still on the Event Dispatch Thread. Finally, the ModelListener will call label.setText(...) on the Event Dispatch Thread. All fine.

But now someone changes the logic: He obtains the model instance, and does a modification of the value in a different thread:

Thread t = new Thread(() -> model.setValue("123"));
t.start();

From the perspective of the caller, this is perfectly fine. He may not even know that a Swing GUI exists in this application. But the setValue call will notify all ModelListeners, on the newly created thread, and among them is one that calls label.setText(...) - now also on the wrong thread.

Therefore, the threading constraints should clearly be documented, and in doubt, one can wrap the modifications to the GUI in something like the executeOnEventDispatchThread method that I sketched above.

Upvotes: 2

fdsa
fdsa

Reputation: 1409

If you are running a multi-threaded swing application then yes, you need to make sure that all of your UI interactions take place on the event dispatch thread. Swing classes are not inherently thread safe, they are only safe because they are confined to a single thread (the event dispatch thread).

Upvotes: 3

Related Questions