Java обновляет компоненты пользовательского интерфейса из другого потока

Я нашел много ответов на свой вопрос, но до сих пор не понимаю, почему мое приложение не выдает никаких исключений. Я создал новое приложение формы Java в NetBeans 8. Моя форма создается и отображается в основном методе следующим образом:

public static void main(String args[])
    {
        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
         */
        try
        {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels())
            {
                if ("Nimbus".equals(info.getName()))
                {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        }
        catch (ClassNotFoundException ex)
        {
            java.util.logging.Logger.getLogger(MainForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        catch (InstantiationException ex)
        {
            java.util.logging.Logger.getLogger(MainForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        catch (IllegalAccessException ex)
        {
            java.util.logging.Logger.getLogger(MainForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        catch (javax.swing.UnsupportedLookAndFeelException ex)
        {
            java.util.logging.Logger.getLogger(MainForm.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>

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

Итак, этот новый Runnable создает новую форму MainForm и делает ее видимой.

Затем в моем коде я запускаю новые потоки, которые обновляют некоторые jButtons и jTextFields. Код ниже:

private void updateUI() {
        updateUIThread = new Thread(() ->
        { 
            while (true) {
                try {
                    jtfIP.setEnabled(!Start && !autoRec);
                    jtfPort.setEnabled(!Start && !autoRec);
                    jtfSlaveID.setEnabled(!Start && !autoRec);
                    jtfTimeout.setEnabled(!Start && !autoRec);
                    jtfReqInterval.setEnabled(!Start && !autoRec);
                    jCheckBox1.setEnabled(!Start && !autoRec);
                    jCBReconnect.setEnabled(!Start && !autoRec);

                    if (db != null) {
                        if (!db.getIsOpen()) {
                            jPBD.setBackground(Color.RED);
                            jPBD.setForeground(Color.WHITE);
                            jPBD.setText("ER");
                        } else {
                            jPBD.setBackground(Color.GREEN);
                            jPBD.setForeground(Color.BLACK);
                            jPBD.setText("OK ");
                        }
                    } else {
                        jPBD.setBackground(Color.RED);
                        jPBD.setForeground(Color.WHITE);
                        jPBD.setText(" ER ");
                    }


                    if (autoRec){
                        jbtnConnect.setText("Auto");
                        if (Start && Connected) {
                            jbtnConnect.setForeground(Color.BLACK);
                            jbtnConnect.setBackground(Color.GREEN);
                        } else {       
                            jbtnConnect.setForeground(Color.WHITE);
                            jbtnConnect.setBackground(Color.RED);
                        }
                    } else {
                        if (Start) {
                            jbtnConnect.setText("Disconnect");
                            jbtnConnect.setForeground(Color.BLACK);
                            jbtnConnect.setBackground(Color.GREEN);

                        } else {
                            jbtnConnect.setText("Connect");
                            jbtnConnect.setForeground(Color.WHITE);
                            jbtnConnect.setBackground(Color.RED);
                        }
                    }

                    jtfErroriCitire.setText(String.valueOf(totalErrors));

                    try
                    {
                        Thread.sleep(300);
                        jPanel4.repaint(1);
                    }
                    catch (InterruptedException ex)
                    {
                        Logger.getLogger(MainForm.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
                catch (Exception ex) {
                    Logger.getLogger(MainForm.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        });
        updateUIThread.start();
    }

И есть другие потоки, начатые подобным образом выше, и где я получаю разные значения, которые обновляются в указанном выше потоке.

Мой вопрос в том, почему мой код не выдает никаких исключений в отношении элементов пользовательского интерфейса, которые обновляются из другого потока? Я НЕ использовал SwingUtilities.invokeLater(new Runnable() { //code here });, и мой код работает отлично...

Благодарю вас!


person serban.b    schedule 21.10.2014    source источник
comment
Поскольку это не так, разработчик должен убедиться, что они правильно синхронизируют обновления с EDT. Вполне вероятно, что решение было принято не отчасти из-за сложности такой конструкции, а отчасти из-за затрат, которые она будет нести с каждым методом компонента, который может изменить состояние, нужно было проверить, находится ли он в правильном потоке.   -  person MadProgrammer    schedule 21.10.2014
comment
Итак, мой код должен использовать SwingUtilities.invokeLater(new Runnable() {}); и мой метод UpdateUI() должен вызываться внутри invokeLater(new Runnable() { updateUI();} или мой метод должен иметь внутри моего потока метод invokeLater, например: private void updateUI() { updateUIThread = new Thread(() - › { while (true) { try { SwingUtilities.invokeLater(new Runnable() // код здесь });   -  person serban.b    schedule 21.10.2014
comment
Если вы хотите обновить пользовательский интерфейс, вы должны делать это в контексте EDT. Хорошим способом сделать это было бы использовать содержимое SwingWorker и либо publish, чтобы быть processed в EDT, либо инициировать события изменения свойства...   -  person MadProgrammer    schedule 21.10.2014


Ответы (3)


Swing не является потокобезопасным и является однопоточным. Вы никогда не должны обновлять компоненты пользовательского интерфейса из-за пределов потока диспетчеризации событий, равно как и никогда не следует запускать длительные процессы или блокировать код в EDT, так как это помешает ему обрабатывать новые события в очереди событий, в результате чего ваше приложение будет выглядеть так, как будто оно завис...потому что он...

Дополнительные сведения см. в разделе Параллелизм в Swing.

Почесав голову некоторое время, я понял, что простым решением было бы просто использовать javax.swing.Timer

Вы хотите повторять обновление с регулярным интервалом (300 миллисекунд) и обновлять пользовательский интерфейс, отлично, Swing Timer способен планировать обновления через регулярные промежутки времени и выполняет обратный вызов в контексте EDT!

Он также имеет возможность консолидировать повторные вызовы. Это означает, что если в очереди событий уже есть действие «таймер», таймер не будет генерировать новое, предотвращая переполнение EDT и возможные проблемы с производительностью...

javax.swing.Timer timer = new Timer(300, new ActionListener() {
    public void actionPerformed(ActionEvent evt) {    
        jtfIP.setEnabled(!Start && !autoRec);
        jtfPort.setEnabled(!Start && !autoRec);
        jtfSlaveID.setEnabled(!Start && !autoRec);
        jtfTimeout.setEnabled(!Start && !autoRec);
        jtfReqInterval.setEnabled(!Start && !autoRec);
        jCheckBox1.setEnabled(!Start && !autoRec);
        jCBReconnect.setEnabled(!Start && !autoRec);

        if (db != null) {
            if (!db.getIsOpen()) {
                jPBD.setBackground(Color.RED);
                jPBD.setForeground(Color.WHITE);
                jPBD.setText("ER");
            } else {
                jPBD.setBackground(Color.GREEN);
                jPBD.setForeground(Color.BLACK);
                jPBD.setText("OK ");
            }
        } else {
            jPBD.setBackground(Color.RED);
            jPBD.setForeground(Color.WHITE);
            jPBD.setText(" ER ");
        }


        if (autoRec){
            jbtnConnect.setText("Auto");
            if (Start && Connected) {
                jbtnConnect.setForeground(Color.BLACK);
                jbtnConnect.setBackground(Color.GREEN);
            } else {       
                jbtnConnect.setForeground(Color.WHITE);
                jbtnConnect.setBackground(Color.RED);
            }
        } else {
            if (Start) {
                jbtnConnect.setText("Disconnect");
                jbtnConnect.setForeground(Color.BLACK);
                jbtnConnect.setBackground(Color.GREEN);

            } else {
                jbtnConnect.setText("Connect");
                jbtnConnect.setForeground(Color.WHITE);
                jbtnConnect.setBackground(Color.RED);
            }
        }

        jtfErroriCitire.setText(String.valueOf(totalErrors));
    }
});
timer.start();

Дополнительные сведения см. в разделе Как использовать таймеры Swing.

person MadProgrammer    schedule 21.10.2014
comment
спасибо за это @MadProgrammer. Я читал о Swing Timers, но не использовал их. Мой подход, который я использовал раньше в С#, я новичок в java, и я обнаружил, что он сильно отличается от С#. - person serban.b; 22.10.2014
comment
@serban.b Разве это не правда :P - person MadProgrammer; 22.10.2014
comment
@MadProgrammer, если вы все еще здесь: это не работает для меня. Задача выполняется правильно с учетом задержки, но по-прежнему не может обновлять элементы графического интерфейса. - person Blaine; 06.05.2017
comment
@Blaine У меня отлично работает - person MadProgrammer; 07.05.2017
comment
@MadProgrammer Хм ... у меня тоже работает в упрощенном коде. Что-то странное с моими другими 700 строками. Странный... - person Blaine; 07.05.2017
comment
@Blaine Рассмотрите возможность публикации вопроса с работающим примером, демонстрирующим вашу проблему. - person MadProgrammer; 07.05.2017
comment
@MadProgrammer да, проблема в том, что он полагается на такое количество библиотек и локальных файлов, что было бы сложно опубликовать что-то, что даже работает без каких-либо дополнений. - person Blaine; 07.05.2017
comment
@Blaine Ну, больше ничего нельзя сделать, возможно, сторонние библиотеки и/или обработка файлов вызывают проблемы, но без какого-либо работающего примера невозможно понять, что предложить. - person MadProgrammer; 07.05.2017

Swing говорит, что вы не должны обновлять компоненты извне Swing Event Dispatch Thread, но не навязывает это. Просто непрактично проверять, из какого потока исходит каждый вызов.

Кроме того, из-за характера проблем, которые, как правило, возникают из-за проблем с потоками (как правило), вы не должны ожидать, что исключения всегда будут выдаваться, когда у вас есть ошибка в многопоточном коде. Это связано с тем, что проблемы с потоками часто приводят к взаимоблокировке или ошибки согласованности памяти, которые в большинстве случаев не подлежат восстановлению (обычно вся JVM просто вылетает).

person allTwentyQuestions    schedule 21.10.2014
comment
Спасибо, теперь ясно. Я читал о SwingWorker (что сказал @MadProgrammer) и обнаружил, что это правильная реализация, но я обнаружил, что можно сделать это с помощью SwingUtilities.invokeLater(new Runnable() public void run() {//updateUI(); } });, что, я думаю, намного проще, чем SwingWorker, но я действительно не знаю, если это 100% безопасно или правильно использовать его. - person serban.b; 21.10.2014
comment
Да, всегда безопасно использовать SwingUtilities.invokeLater(Runnable). Однако SwingWorkers немного отличаются. invokeLater позволяет вам запускать свой код в Swing EDT, а SwingWroker предоставляет вам возможность запускать код в отдельном фоновом потоке, а затем обновлять пользовательский интерфейс из метода done SwingWroker всякий раз, когда фоновый поток завершается. - person allTwentyQuestions; 21.10.2014
comment
@serban.b SwingWorker становится более полезным, когда вам нужно изменить несколько состояний разных компонентов, поскольку он предоставляет простые в использовании методы не только для синхронизации обновлений обратно в EDT (с помощью таких вещей, как publish/process), но и для передачи информации между рабочий и EDT, через поддержку PropertyChangeListener... попробуйте передать переменные с помощью SwingUtilities... - person MadProgrammer; 22.10.2014

И я сделал это.

private void updateUI() {
    updateUIThread = new Thread(() ->
    { 
        while (true) {
            try {
                SwingUtilities.invokeLater(new Runnable() {
                    public void run() {
                        jtfIP.setEnabled(!Start && !autoRec);
                        jtfPort.setEnabled(!Start && !autoRec);
                        jtfSlaveID.setEnabled(!Start && !autoRec);
                        jtfTimeout.setEnabled(!Start && !autoRec);
                        jtfReqInterval.setEnabled(!Start && !autoRec);
                        jCheckBox1.setEnabled(!Start && !autoRec);
                        jCBReconnect.setEnabled(!Start && !autoRec);

                        if (db != null) {
                            if (!db.getIsOpen()) {
                                jPBD.setBackground(Color.RED);
                                jPBD.setForeground(Color.WHITE);
                                jPBD.setText("ER");
                            } else {
                                jPBD.setBackground(Color.GREEN);
                                jPBD.setForeground(Color.BLACK);
                                jPBD.setText("OK ");
                            }
                        } else {
                            jPBD.setBackground(Color.RED);
                            jPBD.setForeground(Color.WHITE);
                            jPBD.setText(" ER ");
                        }


                        if (autoRec){
                            jbtnConnect.setText("Auto");
                            if (Start && Connected) {
                                jbtnConnect.setForeground(Color.BLACK);
                                jbtnConnect.setBackground(Color.GREEN);
                            } else {       
                                jbtnConnect.setForeground(Color.WHITE);
                                jbtnConnect.setBackground(Color.RED);
                            }
                        } else {
                            if (Start) {
                                jbtnConnect.setText("Disconnect");
                                jbtnConnect.setForeground(Color.BLACK);
                                jbtnConnect.setBackground(Color.GREEN);

                            } else {
                                jbtnConnect.setText("Connect");
                                jbtnConnect.setForeground(Color.WHITE);
                                jbtnConnect.setBackground(Color.RED);
                            }
                        }

                        jtfErroriCitire.setText(String.valueOf(totalErrors));
                    }
                });
                try
                {
                    Thread.sleep(300);
                    jPanel4.repaint(1);
                }
                catch (InterruptedException ex)
                {
                    Logger.getLogger(MainForm.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            catch (Exception ex) {
                Logger.getLogger(MainForm.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    });
    updateUIThread.start();
}

Я поместил свой код обновления пользовательского интерфейса в метод запуска invokeLater. updateUI() вызывается при запуске приложения.

person serban.b    schedule 21.10.2014
comment
Я был бы осторожен с таким подходом, так как он может затопить запросы EDT, что приведет к снижению производительности приложения. - person MadProgrammer; 22.10.2014
comment
это вызвало что-то ... У меня 5-12% использования процессора (в TaskManager, под Windows 7 64bit) и около 125-155 МБ памяти. Попробую с Swing Timers и выложу результаты через несколько дней, когда вернусь из командировки. наилучшие пожелания - person serban.b; 22.10.2014
comment
С таким подходом не обязательно с этим примером. Такой подход может затопить EDT обновлениями, особенно если эти обновления будут достаточно быстрыми или многочисленными. - person MadProgrammer; 22.10.2014