通过不同的线程更新java UI

时间:2015-08-22 00:03:26

标签: java multithreading swing model-view-controller

我要开发一个swing应用程序,我使用的是 MVC 模式的简化版本,其中控制器插入到视图和模型之间的两个方向:

  • 当用户与挥杆控件交互时,如果此交互需要访问模型,则由swing控件引发的事件需要适当的控制器方法;
  • 在模型更新后应更新视图时,控制器会调用视图的一个或多个公共方法。

复杂的应用程序可能会请求运行不同的线程:例如,如果必须在磁盘上写入文件,则可以启动后台线程,并且在写入文件结束时,此线程应发送通知以查看通过控制器。在这种情况下,可能会发生多个线程想要刷新视图,因此我认为应该以某种方式处理此问题。

我从this answer中获取灵感,以便编写以下SSCCE的方法appendTextupdateLastText

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();
    }
}

上述解决方案似乎工作正常,我应该使用相同的技术来更新UI的其他swing组件吗?还是有更紧凑的解决方案?

2 个答案:

答案 0 :(得分:3)

如果您正在运行多线程swing应用程序,那么您需要确保所有UI交互都在event dispatch thread上进行。 Swing类本身并不是线程安全的,它们只是安全的,因为它们仅限于一个线程(事件派发线程)。

答案 1 :(得分:2)

关于是否应该对所有Swing组件更新使用此方法的一般问题:是的,您应该这样做。

关于是否有更紧凑的解决方案的问题:我没有意识到这一点。一般情况下有事件处理的基础结构(例如Guava EventBus),但是没有一个能够从仔细考虑哪个线程什么(并且Swing组件上的所有操作都由EDT完成,特别是)。

这些调用只能稍微“更紧凑”,因为Runnable通常可以在Java 8中写成lambda。

附注:您应该决定使用EventQueueSwingUtlities。我更喜欢后者,但这些类的invokeLater方法是等价的。只是交替使用它们可能会令人困惑。

另一方面注意:有时候检查意图行动是否已经在事件派遣线程上执行是值得的。例如,您可以考虑编写这样的一种或另一种方法:

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);
    }
}

在事件派发线程上调用appendText时,它将直接在文本字段中设置文本。当从不同的线程调用它时,它将放置任务以更新事件队列上的文本。

  

编辑以回复评论

有时候从正确的线程进行某种修改是“显而易见的”。如果要在文本字段中附加文本,例如,从附加到按钮的actionPerformed ActionListener方法中添加文本,则此actionPerformed方法将在事件派发线程 - 所以没有必要考虑线程问题。

但有时您将GUI直接或间接作为侦听器附加到某些数据模型。并且您不知道在此数据模型上发生哪些线程修改。例如:想象一个简单的数据模型

class Model {

    void addModelListener(ModelListener listener) { ... }

    String getValue() { ... }

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

现在,一些GUI组件附加到此,以便在某些标签中显示“值”:

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

只要模型仅在Event Dispatch Thread 上修改,这就完全没问题了。例如:

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

按下该按钮时,将在事件发送线程上执行ActionListener。对setValue的调用将依次通知事件调度线程中的所有ModelListener。最后,ModelListener将在事件调度线程上调用label.setText(...)。一切都很好。

但是现在有人更改了逻辑:他获取了model实例,并对不同的线程中的值进行了修改:

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

从来电者的角度来看,这非常好。他甚至可能不知道此应用程序中存在Swing GUI 。但setValue调用将在新创建的线程上通知所有ModelListeners,其中一个调用label.setText(...) - 现在也在错误的线程上。

因此,应该清楚地记录线程约束,并且毫无疑问,可以将修改包含在GUI中,例如我在上面概述的executeOnEventDispatchThread方法。