【问题标题】:Busy loop in other thread delays EDT processing其他线程中的繁忙循环延迟 EDT 处理
【发布时间】:2016-05-11 07:26:43
【问题描述】:

我有一个 Java 程序,它在一个单独的(非 EDT)线程上执行一个紧密循环。尽管我认为 Swing UI 仍然应该是响应式的,但事实并非如此。下面的示例程序显示了这个问题:单击“试试我”按钮应该会在半秒后或多或少地弹出一个对话框,并且应该可以通过单击它的任何响应来立即关闭该对话框。相反,对话框需要更长的时间才能出现,和/或在单击其中一个按钮后需要很长时间才能关闭。

  • 在 Linux(具有不同发行版的两台不同机器)、Windows、Raspberry Pi(仅限服务器 VM)和 Mac OS X(由另一个 SO 用户报告)上出现问题。
  • Java 版本 1.8.0_65 和 1.8.0_72(都试过了)
  • i7 处理器多核。 EDT 应该有足够的备用处理能力。

有没有人知道为什么 EDT 处理会被延迟,即使只有一个繁忙的线程?

(请注意,尽管Thread.sleep 调用的各种建议是问题的原因,但它不是。它可以被删除并且问题仍然可以重现,尽管它出现的频率略低并且通常表现出第二个上述行为 - 即无响应 JOptionPane 对话框而不是延迟对话框出现。此外,睡眠调用没有理由让给其他线程,因为如上所述有备用处理器内核 ; 在调用 sleep 之后,EDT 可以继续在另一个内核上运行。

import java.awt.EventQueue;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;

public class MFrame extends JFrame
{
    public static void main(String[] args)
    {
        EventQueue.invokeLater(() -> {
            new MFrame();
        });
    }

    public MFrame()
    {
        JButton tryme = new JButton("Try me!");

        tryme.addActionListener((e) -> {
            Thread t = new Thread(() -> {
                int a = 4;
                for (int i = 0; i < 100000; i++) {
                    for (int j = 0; j < 100000; j++) {
                        a *= (i + j);
                        a += 7;
                    }
                }
                System.out.println("a = " + a);
            });

            t.start();

            // Sleep to give the other thread a chance to get going.
            // (Included because it provokes the problem more reliably,
            // but not necessary; issue still occurs without sleep call).
            try {
                Thread.sleep(500);
            }
            catch (InterruptedException ie) {
                ie.printStackTrace();
            }

            // Now display a dialog
            JOptionPane.showConfirmDialog(null, "You should see this immediately");
        });

        getContentPane().add(tryme);

        pack();
        setVisible(true);
    }
}

更新:该问题仅出现在服务器虚拟机上(但请参阅进一步更新)。指定客户端 VM(-client java 可执行文件的命令行参数)似乎可以抑制问题(update 2)在一台机器上而不是另一台机器上。

更新 3:单击按钮后,我看到 Java 进程的处理器使用率为 200%,这意味着有 2 个处理器内核已完全加载。这对我来说根本没有意义。

更新 4:也出现在 Windows 上。

更新 5: 使用调试器 (Eclipse) 被证明是有问题的;调试器似乎无法停止线程。这是非常不寻常的,我怀疑 VM 中存在某种活锁或竞争条件,所以我向 Oracle 提交了一个错误(审查 ID JI-9029194)。

更新 6: 我找到了my bug report in the OpenJDK bug database。 (我没有被告知它已被接受,我不得不搜索它)。那里的讨论最有趣,并且已经阐明了可能导致此问题的原因。

【问题讨论】:

  • 这是sleep。睡眠调用正在让步给繁忙的线程。并且 sleep/wait 只保证“至少”在它们返回时已经过了请求的时间。
  • @VGR 我不这么认为。你试过了吗?
  • 是的。当我注释掉睡眠时,JOptionPane 立即出现。 (Linux amd64,四核。)
  • @VGR 立即出现,但是否响应?
  • @davmac 清晨观察,耐心不是特别高:P

标签: java multithreading swing


【解决方案1】:

这似乎是个问题。下面是观察Event Dispatcher Thread延迟处理,应该立即响应:

  1. 执行示例程序
  2. 点击“试试我”按钮
  3. 单击任意按钮(是/否/取消)以关闭结果对话框

在 Windows 上

第 2 步和第 3 步之间的长时间延迟观察。

第 3 步 -> 立即关闭对话框。

在 Linux 上

第 2 步到第 3 步 - 没有延迟。

第 3 步 -> 长时间关闭对话框。

【讨论】:

    【解决方案2】:

    我的观察:

    • 用 swingworker 替换线程:

      • 没有区别
    • 用 swingworker 替换线程并在第一个 for 循环中做一些工作:

      • 得到了预期的 UI 没有冻结的结果,从这里开始一切顺利

    使用此代码,可以观察到预期的行为:

    public class MFrame extends JFrame {
        public static void main(String[] args) {
            new MFrame();
        }
    
        public MFrame() {
            JButton tryme = new JButton("Try me!");
    
            tryme.addActionListener((e) -> {
                SwingWorker<Void, Void> longProcess = new SwingWorker<Void, Void>() {
                    private StringBuilder sb = new StringBuilder();
    
                    @Override
                    protected Void doInBackground() throws Exception {
                        int a = 4;
                        for (int i = 0; i < 100000; i++) {
                            for (int j = 0; j < 100000; j++) {
                                a *= (i + j);
                                a += 7;
                            }
                            sb.append(a); // <-- this seems to be the key
                        }
                        System.out.println("a = " + a);
                        return null;
                    }
    
                    @Override
                    protected void done() {
                        try {
                            get();
                            System.out.println(sb.toString());
                        } catch (InterruptedException | ExecutionException e1) {
                            e1.printStackTrace();
                        }
                    }
                };
    
                longProcess.execute();
    
                // Sleep to give the other thread a chance to get going.
                // (Included because it provokes the problem more reliably,
                // but not necessary; issue still occurs without sleep call).
                try {
                    Thread.sleep(500);
                }
                catch (InterruptedException ie) {
                    ie.printStackTrace();
                }
    
                // Now display a dialog
                SwingUtilities.invokeLater(() -> JOptionPane.showConfirmDialog(this, "You should see this immediately"));
            });
    
            getContentPane().add(tryme);
            pack();
            setLocationRelativeTo(null);
            setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            setVisible(true);
        }
    }
    

    使用 OP 的原始代码进行同样的观察:

    • 在第一个 for 循环中做一些其他工作
      • 得到了预期的结果

    例子

    tryme.addActionListener((e) -> {
    
        Thread t = new Thread(() -> {
            StringBuilder sb = new StringBuilder();
    
            int a = 4;
            for (int i = 0; i < 100000; i++) {
                for (int j = 0; j < 100000; j++) {
                    a *= (i + j);
                    a += 7;
                }
                sb.append(a); // <-- again the magic seems to be on this line
            }
            System.out.println(sb.toString());
        });
        ...
    });
    

    我在一台功能强大的机器上运行 Ubuntu 14.04。

    java version "1.8.0_72"
    Java(TM) SE Runtime Environment (build 1.8.0_72-b15)
    Java HotSpot(TM) 64-Bit Server VM (build 25.72-b15, mixed mode)
    

    我的观察意味着什么?

    除了所有的东西都没有丢失之外,没有什么其他的东西,而且有人可能对编译器进行了过多的优化,这使得它以某种方式阻塞了 UI 线程。老实说,我不确定这一切意味着什么,但我相信有人会弄清楚

    【讨论】:

    • 有趣。是的,嵌套循环结构的具体形式似乎是引发问题的原因。我怀疑做任何需要任何同步的事情(例如,使用 StringBuilder 所需的内存分配)都会防止问题浮出水面。
    • 与StringBuffer 不同,StringBuilder 未同步。关于结构的好点;优化器可能会干扰抢占。
    • @trashgod 我知道StringBuilder 方法没有同步。当我说“任何类型的同步”时,我不仅仅指“Java 级别的同步块或方法”——我指的是任何需要在 some 级别的线程之间进行同步的东西,例如我提到的堆分配在我上面的评论中。但是你这里关于抢占的说法是不正确的。有可用的内核,因此不需要抢占,因为线程可以(或至少应该能够)在单独的内核上独立运行 - 如果它们不能,那么问题是“为什么不呢?”仍然存在。
    【解决方案3】:

    我在 Mac OS X 上看到了同样的效果。尽管您的示例已正确同步,但您看到的平台/JVM 可变性可能是由于线程调度方式的变幻莫测,导致饥饿。将Thread.yield() 添加到t 的外循环可以缓解该问题,如下所示。除了示例的人工性质外,通常需要Thread.yield() 之类的提示not。在任何情况下,请考虑 SwingWorker,如图所示 here 执行类似的紧密循环以用于演示目的。

    我认为在这种情况下根本不需要调用Thread.yield(),尽管测试用例是人为的。

    正确; yielding 只是暴露了潜在的问题:tstarves 事件调度线程。请注意,即使没有Thread.yield(),以下示例中的 GUI 也会迅速更新。如相关Q&A 中所述,您可以尝试降低线程的优先级。或者,在单独的 JVM 中运行 t,如建议的使用 ProcessBuilder 的 here,也可以在 SwingWorker 的后台运行,如 here 所示。

    但是为什么?

    所有支持的平台都建立在单线程图形库上。阻塞、饿死或饱和管理事件调度线程是相当容易的。重要的后台任务通常会隐式产生,例如在发布中间结果、阻塞 I/O 或等待工作队列时。 不做的任务可能必须明确地让步。

    import java.awt.EventQueue;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    
    public class MFrame extends JFrame {
    
        private static final int N = 100_000;
        private static final String TRY_ME = "Try me!";
        private static final String WORKING = "Working…";
    
        public static void main(String[] args) {
            EventQueue.invokeLater(new MFrame()::display);
        }
    
        private void display() {
            JButton tryme = new JButton(TRY_ME);
            tryme.addActionListener((e) -> {
                Thread t = new Thread(() -> {
                    int a = 4;
                    for (int i = 0; i < N; i++) {
                        for (int j = 0; j < N; j++) {
                            a *= (i + j);
                            a += 7;
                        }
                        Thread.yield();
                    }
                    EventQueue.invokeLater(() -> {
                        tryme.setText(TRY_ME);
                        tryme.setEnabled(true);
                    });
                    System.out.println("a = " + a);
                });
                t.start();
                tryme.setEnabled(false);
                tryme.setText(WORKING);
            });
            add(tryme);
            pack();
            setLocationRelativeTo(null);
            setDefaultCloseOperation(EXIT_ON_CLOSE);
            setVisible(true);
        }
    }
    

    【讨论】:

    • 感谢您的确认。这意味着该问题可以在 Windows、Linux、Mac OS X 和 Raspberry Pi(我同时测试过)上可靠地重现。我认为在这种情况下根本不需要调用Thread.yield(),尽管测试用例的人为性质,但是。有两个线程(EDT 和创建的线程)不应该争用任何资源,并且有足够多的备用处理器内核来运行这两个线程。
    • 我已经在上面详细说明了。
    • "t 饿死事件调度线程" - 好的,但是为什么?有备用的处理器内核,因此 EDT 可以轻松地在另一个内核上运行;两个线程之间不应该有任何争用。为什么我看到两个线程(大概是t 和EDT)都消耗了 100% 的内核?对我来说,这开始看起来像是 JVM 中的活锁问题。
    • 我不确定;我会专注于为什么后台任务会失败。
    • 你误会了我,我想。如果有其他内核空闲,它应该不需要让出处理器内核。
    【解决方案4】:

    这不是最终答案,但它更接近于理解问题。

    我尝试最小化代码以消除 sleep 和 actionPerformed 的潜在缺陷,我相信我已经这样做了,同时保持问题完好无损:

    public class MFrame extends JFrame {
    
        public static void main(String[] args) {
    
            EventQueue.invokeLater(() -> new MFrame());
        }
    
        public MFrame() {
    
            Thread t = new Thread(() -> {
                int a = 4;
                for (int i = 0; i < 50000; i++) {
                    for (int j = 0; j < 50000; j++) {
                        a *= (i + j);
                        a += 7;
                    }
                }
    //          System.out.println("c" + a);
            });
    
            System.out.println("a");
    //      pack();
            t.start();
    //      pack();
            System.out.println("b");
        }
    }
    

    在 Win7、i7 2670QM、JDK 1.8.0_25 上我得到以下结果:

    只有第二个pack 被注释掉了:

    a
    b [pause]
    c-1863004573
    

    (预计即使没有同步,打印b 也会在打印c 之前达到,除非您使用的超级处理器可以更快地进行计算?)

    只有第一个 pack 被注释掉了:

    a [pause]
    c-1863004573
    b
    

    (不是预期的)

    你能用/不带-client标志确认我的结果吗?

    【讨论】:

    • 有一个有趣的问题,JFrame 必须是可见的,否则无法再现绘画制品,也无法阻止 EDT,在这种情况下,EDT 在几秒钟后消失(标准),返回false,在执行最后一行代码后再次返回 true(标准),这只是线性代码执行,如果 EDT 消失或不消失,则存在风险
    • 尝试将 OP 的 for-loop 正确重定向到工作线程,例如通过使用 Runnable#Thread,普通的 Thread 和 EDT 在这里是百万次
    • 恐怕我在所有情况下都会得到相同的输出 - a、b、(暂停)、c,无论是否存在pack
    • (我假设问题是由某些特定时间触发的。它在不同机器上的行为略有不同;这我已经确定了。我认为弄乱示例的形式不会老实说,帮助很大)。
    • @davmac 如果您在示例中删除线程中的打印行,是否没有区别?
    【解决方案5】:
    • 默认情况下,一切都从 EDT 开始(在这种情况下,在 ActionListener 内),由 Thread.sleep 锁定,在所有代码执行时结束,包括 Thread.sleep,假设您丢失了所有事件,包括。绘制,在这段代码的整个时间内,所有事件都在最后一次绘制

    • 此代码丢失了自动关闭JOptionPane,永远不会被绘制到屏幕上(模拟如何舒适地Thread.sleep 杀死 Swing 中的绘制)

    • Swing GUI 对鼠标或按键事件不负责任,无法终止此应用程序,这仅可以从 Runnable#Thread 和 SwingWorker 开始,指定启动新的另一个线程 (@987654328 @),在 Runnable#Thread 和 SwingWorker 内的任何内容都可以取消任务(或者使用 Runnable#Thread 可以暂停、修改...)

    • 这不是关于多线程,也不是关于将资源共享给另一个核心,在 Win10 中,所有核心都向我展示,按比例共享增量

    (稍作修改)代码的输出(基于您的 SSCCE / MCVE)

    run:
    test started at - 16:41:13
    Thread started at - 16:41:15
    to test EDT before JOptionPane - true at 16:41:16
    before JOptionPane at - 16:41:16
    Thread ended at - 16:41:29
    a = 1838603747
    isEventDispatchThread()false
    after JOptionPane at - 16:41:29
    Thread started at - 16:41:34
    to test EDT before JOptionPane - true at 16:41:34
    before JOptionPane at - 16:41:34
    Thread ended at - 16:41:47
    a = 1838603747
    isEventDispatchThread()false
    after JOptionPane at - 16:41:47
    BUILD SUCCESSFUL (total time: 38 seconds)
    

    再次自动关闭 JOptionPane 永远不会被绘制到屏幕上(测试 win10-64b、i7、Java8),可能直到 Java 1.6.022 一切都将被正确绘制(AFAIK 是对 edt 的最后修复,从这个时候开始SwingWorker 没有错误)

    import java.awt.Component;
    import java.awt.EventQueue;
    import java.awt.Window;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import javax.swing.AbstractAction;
    import javax.swing.Action;
    
    import javax.swing.JButton;
    import javax.swing.JDialog;
    import javax.swing.JFrame;
    import javax.swing.JOptionPane;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    
    public class MFrame extends JFrame {
    
        public static void main(String[] args) {
            EventQueue.invokeLater(() -> {
                new MFrame();
            });
        }
    
        public MFrame() {
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
            System.out.println("test started at - " + sdf.format(getCurrDate().getTime()));
            //http://stackoverflow.com/a/18107432/714968
            Action showOptionPane = new AbstractAction("show me pane!") {
                private static final long serialVersionUID = 1L;
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    createCloseTimer(3).start();
                    System.out.println("before JOptionPane at - "
                            + sdf.format(getCurrDate().getTime()));
                    JOptionPane.showMessageDialog((Component) e.getSource(), "nothing to do!");
                }
    
                private Timer createCloseTimer(int seconds) {
                    ActionListener close = new ActionListener() {
    
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            Window[] windows = Window.getWindows();
                            for (Window window : windows) {
                                if (window instanceof JDialog) {
                                    JDialog dialog = (JDialog) window;
                                    if (dialog.getContentPane().getComponentCount() == 1
                                            && dialog.getContentPane().getComponent(0) instanceof JOptionPane) {
                                        dialog.dispose();
                                        System.out.println("after JOptionPane at - "
                                                + sdf.format(getCurrDate().getTime()));
                                    }
                                }
                            }
                        }
                    };
                    Timer t = new Timer(seconds * 1000, close);
                    t.setRepeats(false);
                    return t;
                }
            };
            JButton tryme = new JButton("Try me!");
            tryme.addActionListener((e) -> {
                System.out.println("Thread started at - "
                        + sdf.format(getCurrDate().getTime()));
                Thread t = new Thread(() -> {
                    int a = 4;
                    for (int i = 0; i < 100000; i++) {
                        for (int j = 0; j < 100000; j++) {
                            a *= (i + j);
                            a += 7;
                        }
                    }
                    System.out.println("Thread ended at - "
                            + sdf.format(getCurrDate().getTime()));
                    System.out.println("a = " + a);
                    System.out.println("isEventDispatchThread()" + 
                            SwingUtilities.isEventDispatchThread());
                });
                t.start();
                // Sleep to give the other thread a chance to get going:
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                }
                // Now display a dialog
                System.out.println("to test EDT before JOptionPane - "
                        + SwingUtilities.isEventDispatchThread()
                        + " at " + sdf.format(getCurrDate().getTime()));
                showOptionPane.actionPerformed(e);
            });
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            add(tryme);
            pack();
            setLocation(150, 150);
            setVisible(true);
        }
    
        private Date getCurrDate() {
            java.util.Date date = new java.util.Date();
            return date;
        }
    }
    

    注意也必须使用Runnable#Thread 和SwingWorker 进行测试

    【讨论】:

    • 我担心语言在这里是一个障碍,但我认为你可能误解了这个问题。 Thread.sleep() 不是问题的关键,您可以将其从我的 MCVE 中删除,它仍然会出现问题。
    • 只有当 EDT 中的所有代码行都被执行时,Swing 中的所有绘画才完成(并且在某一刻,最后),然后 EDT 将绘画刷新到 GPU,并且选择使用线程。 sleep,它可以清除屏幕上的所有绘画,什么都不会被绘画,你有丢失所有事件或部分事件的风险
    • 我不相信使用 Thread.sleep 实际上会导致绘制事件丢失,但即使它确实如此,正如我已经说过的那样,可以从我的示例中删除 Thread.sleep 并且它仍然存在问题(通常是稍作修改的形式)- 选项窗格对话框没有响应。
    • 在我的情况下,我丢失了 JOPtionPane,请参阅(最好在你的 VM 中测试)thread ended at 和 after JOptionPane at 的时间,并且 isEventDispatchThread() 必须返回 true,这就是你的问题,解释 EDT在 EDT 中的所有事件都完成后返回 false(true 是几秒钟后),这是所有单线程编程语言(包括 MsAccess)的标准
    • @mKorbel:同意SwingWorker;不确定JOptionPane 如何处理饥饿问题。
    猜你喜欢
    • 2017-05-02
    • 1970-01-01
    • 2016-07-29
    • 2017-05-26
    • 2017-01-18
    • 1970-01-01
    • 1970-01-01
    • 2021-12-20
    • 2017-04-30
    相关资源
    最近更新 更多