【问题标题】:GUI Threading in Java (and SwingUtilities)Java 中的 GUI 线程(和 SwingUtilities)
【发布时间】:2012-03-19 10:40:01
【问题描述】:

我正在使用 swing 用 Ja​​va 制作一个简单的游戏,并且在按下应该触发 JPanel 中的开关的按钮后,我的 GUI 偶尔会冻结(很可能是由于线程问题)。

我发布了一个相关线程here,其中包含有关我当前正在使用的实际代码的更多详细信息(尽管我确实更新了倒计时并使其正常工作)。从该线程的答案来看,似乎 usingSwingUtilities.invokeLater()invokeAndWait() 可能是我解决问题所需要的,但我不确定在我的代码中哪里有必要或确切地如何实现它。

我对线程的了解并不多,可以使用任何我能获得的帮助(最好有点详细并带有一些示例代码)。让我知道是否有任何进一步的细节有用。

【问题讨论】:

  • 您是否在事件线程(EDT)上调用Thread.sleep(...)
  • 不,不再。我重写了之前这样做的部分代码。
  • @scae 你能分享你更新组件响应的代码吗?
  • @CengizCan 您是否查看了我在问题中链接到的页面上发布的代码?这基本上包含了与这个问题相关的所有代码。
  • 如果您的 GUI 冻结,您可以使用 jstack 获取线程转储(使用 jps 查找进程 ID),或从 sonsole ctrl-break (Windows) 或 ctrl-3/`ctrl- `(IIRC,Linux)。

标签: java multithreading swing user-interface freeze


【解决方案1】:

见:Tutorial: Concurrency in Swing

一般来说,Eve​​nt Dispatch Thread 是一个单线程,在事件队列中穿梭,一次处理一个。

SwingUtilities.invokeLater(..) 

在此队列上放置一个 Runnable。因此,当 EDT 完成它之前的队列中的所有内容时,它将由 EDT 处理(这就是为什么在队列上休眠会阻止其他事件,如重绘)。从 EDT 本身调用 invokeLater(..) 相对不常见,尽管在某些情况下它很有用(通常作为 hack)。我认为在过去 6 年中我没有合法使用 SwingUtilities.invokeAndWait(..)。也许一次。

javax.swing.Timer 可以配置为触发一次或定期触发。当它触发时,它会在 EDT 队列上放置一个事件。如果您需要进行计算密集型处理,请考虑使用javax.swing.SwingWorker 在另一个线程上进行计算,并以线程安全的方式将结果返回给您(这也比较少见)。

【讨论】:

  • 感谢您的解释。不过,您能否帮我弄清楚我的代码中有什么问题,以及如何解决它(我不太确定 EDT 在哪里开始/结束,以及我需要在哪里使用 invokeLater() 来添加一些东西到 EDT 队列)?
  • 尝试创建一个 SSCCE (sscce.org) 来演示您的问题。提炼问题可能会帮助您自己解决问题,否则,将其发布在此处,人们将能够为您提供更具体的建议。
  • 谢谢!我这样做了,结果发现是我认为完全不相关的东西(非 GUI 的东西)导致了冻结。
【解决方案2】:

docs 是个不错的选择。在您的情况下,这解释了 SwingUtilities.invokeLater() 的工作原理以及在何处使用它:

导致 doRun.run() 在 AWT 事件上异步执行 调度线程。 应用时应使用此方法 线程需要更新 GUI

因此,在修改 GUI 的操作中,您必须使用 invokeLater 方法来确保 GUI 不会冻结。

另一个很好的资源是 Java 教程。他们覆盖concurrency in Swing

【讨论】:

  • 好的,我知道我需要使用 invokeLater() 通过在 EDT 队列中添加一些代码来停止冻结,但是我需要在我的代码中的哪个位置执行此操作?我尝试将它添加到我认为有意义的几个不同的地方,但我的想法都没有解决它,所以我显然遗漏了一些东西(代码示例/程序大纲请参阅我的另一篇文章)。
【解决方案3】:

如果您在 GUI 代码中定义了一些这样的工作

Runnable doWorkRunnable = new Runnable() {
    @Override
    public void run() { 
        doWork(); 
    }
};

你通过将它附加到一个新的Thread来调用它

Thread t = new Thread(doWorkRunnable);
t.start();

您正在 GUI 线程中执行您的工作,这将导致 Swing 应用程序出现问题。

试试这个(让我提一下这只是一个使用示例)

SwingUtilities.invokeLater(doWorkRunnable);

这会将您的 Runnable 工作人员放入 AWT 事件队列,并在之前的事件完成时执行它。

编辑:这是完整的示例,它从 3 到 0 执行倒计时,然后在倒计时后执行您想做的任何事情。

public class TestFrame extends JFrame {

    private JPanel contentPane;
    private final Timer timer;
    private TimerTask[] tasks;

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    TestFrame frame = new TestFrame();
                    frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public TestFrame() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(100, 100, 450, 300);
        contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        contentPane.setLayout(new BorderLayout(0, 0));
        final JLabel lblCountdown = new JLabel();
        contentPane.add(lblCountdown, BorderLayout.NORTH);
        JButton btnStart = new JButton("Start");
        contentPane.add(btnStart, BorderLayout.SOUTH);

        timer = new Timer();
        tasks = new TimerTask[4];

        setContentPane(contentPane);

        for (int i = 0; i < 4; i++) {
            final int count = i;
            tasks[i] = new TimerTask() {
                public void run() {
                    EventQueue.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            lblCountdown.setText(count + "");
                        }
                    });
                }
            };
        }

        btnStart.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {

                for (int i = 0; i < 4; i++) {
                    timer.schedule(tasks[4 - i - 1], (1000 * i), (1000 * (i + 1)));
                }
                // add another timer.schedule(TimerTask)
                // to execute that "move to game screen" task
                TimerTask taskGotoGame = new TimerTask() {
                    public void run() {
                        timer.cancel();
                        JOptionPane.showMessageDialog(null, "Go to game", "Will now", JOptionPane.INFORMATION_MESSAGE);
                        System.exit(0);
                    }
                };
                // and schedule it to happen after ROUGHLY 3 seconds
                timer.schedule(taskGotoGame, 3000);
            }
        });

    }

}

【讨论】:

  • 感谢您的概述,但是我已经知道我可能不得不使用 invokeLater(),我只是不知道在哪里,我正在寻找更具体到我的代码的帮助(我已经提到我停止使用 Thread.sleep(),所以这不会导致问题)。您能否查看我在问题中链接到的帖子中的实际代码/程序大纲并给我更多相关反馈?
  • 我在几个地方提到过,在程序成功切换 JPanel 的情况下,我已经让倒计时工作得很好。问题是让最终显示倒计时的面板甚至变得可见(并且在此之前没有在按钮按下时冻结 GUI)。即使我实际上没有访问任何倒计时代码并且只是添加面板并尝试使其可见,这也是一个问题(也就是说,在我的示例代码中,如果我注释掉 levelPanel.start() 调用它仍然会冻结)。
  • 我添加了一个完整的工作示例。我们定义了 4 个TimerTasks,其中包含Runnables,将JLabel 更新为特定数字,另一个结束任务停止Timer,并在倒计时完成后执行您想要执行的操作。我省略了“切换到游戏面板”部分。阅读主要方法以了解要做什么。
【解决方案4】:

我创建了一个 WorkerThread 类来处理线程和 GUI 当前/主线程。当事件触发以启动 XXXServer 时,我已将我的 GUI 应用程序放在 WorkerThread 的construct() 方法中,然后所有线程都被激活并且 GUI 工作顺利,而不会冻结。看一看。 /** * 动作事件 * * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) */ 公共无效actionPerformed(ActionEvent ae){ log.info("actionPerformed begin..." + ae.getActionCommand());

    try {
        if (ae.getActionCommand().equals(btnStart.getText())) {
             final int portNumber = 9990;
             try {

                 WorkerThread workerThread = new WorkerThread(){
                    public Object construct(){

                        log.info("Initializing the XXXServer ...");
                        // initializing the Socket Server
                         try {
                            XXXServer xxxServer = new XXXServer(portNumber);
                            xxxServer.start();
                            btnStart.setEnabled(false);                             
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            log.info("actionPerformed() Start button ERROR IOEXCEPTION..." + e.getMessage());
                            e.printStackTrace();
                        }
                        return null;
                    }
                };workerThread.start();
                } catch (Exception e) {
                    log.info("actionPerformed() Start button ERROR..." + e.getMessage());
                    e.printStackTrace();
             }


        } else if (ae.getActionCommand().equals(btnStop.getText())) {
            log.info("Exit..." + btnStop.getText());
            closeWindow();
        }

    } catch (Exception e) {
        log
            .info("Error in ServerGUI actionPerformed==="
                + e.getMessage());
    }

}

【讨论】:

    【解决方案5】:

    为了在现有的 WorkerThread 中调用一个动作,可以直观地使用 SwingUtilities.invokeLater() 将用户定义的事件发送到 JFrame 的 actionPerformed() 方法

    class TestFrame extends JFrame implements ActionListener
    {
    ...
    
        private class Performer implements Runnable
        {
            ActionEvent event;
    
            Performer(ActionEvent event)
            {
                this.event = event;
            }
    
            @Override
            public void run()
            {
                actionPerformed(event);
            }
    
        }
    
        synchronized protected void invokeLater(ActionEvent event)
        {
            SwingUtilities.invokeLater(new Performer(event));
        }
    
        public void actionPerformed(ActionEvent event)
        {
            ...
        }
    
    }
    

    现在,在任何线程中调用的 TestFrame.invokeLater() 都将在现有 WorkerThread 中的 TestFrame.actionPerformed() 中处理。

    【讨论】:

      猜你喜欢
      • 2012-06-16
      • 2012-03-18
      • 2013-07-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多