【问题标题】:Update java UI by different threads通过不同的线程更新 java UI
【发布时间】:2015-11-16 00:36:47
【问题描述】:

我为了开发一个摇摆应用程序,我使用的是 MVC 模式的简化版本,其中控制器在两个方向上都介于视图和模型之间:

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

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

我从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 的其他摆动组件吗?还是有更紧凑的解决方案?

【问题讨论】:

    标签: java multithreading swing model-view-controller


    【解决方案1】:

    关于是否应该对所有 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时,会直接在文本字段中设置文本。当从不同的线程调用它时,它会将更新文本的任务放在事件队列中。

    根据评论进行编辑

    有时“很明显”某个修改是从正确的线程完成的。当您想在文本字段中附加文本时,例如,从附加到按钮的ActionListeneractionPerformed 方法,那么这个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()) );
    

    这很好只要模型只在事件调度线程上修改。例如:

    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 方法中。

    【讨论】:

    • 如果 EDT 需要与任何其他线程同时更新摇摆控件(例如上述代码 sn-ps 的 textFieldjTextArea1),非 EDT线程 必须通过SwingUtilities.invokeLater(runnable) 执行更新,但EDT 可以直接访问swing 控件。对吗?
    • 假设我们修改我的sample code,增加一个JButton jButton1,这样当按下这个按钮的时候,字符串"Hello World!"就追加到jTextArea1上:方法jButton1ActionPerformed(ActionEvent evt)可以直接访问jTextArea1,因此不需要使用方法appendText(因为方法jButton1ActionPerformed已经在EDT上执行了),所有这些都没有方法jButton1ActionPerformed和方法@之间的竞争条件的风险987654356@。这是正确的吗?
    • 这是正确的。但请注意,这并不总是那么微不足道。我为此添加了一个 EDIT。
    【解决方案2】:

    如果您正在运行一个多线程的 Swing 应用程序,那么可以,您需要确保您的所有 UI 交互都发生在 event dispatch thread 上。 Swing 类本身并不是线程安全的,它们只是安全的,因为它们被限制在单个线程(事件调度线程)中。

    【讨论】:

      猜你喜欢
      • 2015-01-10
      • 2010-10-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多