繁体   English   中英

其他线程中的繁忙循环延迟 EDT 处理

[英]Busy loop in other thread delays EDT processing

我有一个 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对话框而不是延迟的对话框出现。此外,睡眠调用没有理由让给另一个线程,因为如上所述有空闲的处理器内核;EDT 可以继续在另一个内核上运行打电话sleep )。

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 上(但请参阅进一步更新)。 指定客户端 VM(java 可执行文件的-client命令行参数)似乎可以抑制一台机器上的问题(更新 2,而不是另一台机器

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

更新 4:也发生在 Windows 上。

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

更新 6:在 OpenJDK 错误数据库中找到了我的错误报告 (我没有被告知它已被接受,我不得不搜索它)。 那里的讨论最有趣,并且已经阐明了可能导致此问题的原因。

我看到了与Mac OS X相同的效果。虽然您的示例已正确同步,但您看到的平台/ JVM可变性可能是由于线程如何安排的变幻莫测,导致饥饿。 Thread.yield()添加到t的外部循环可以缓解问题,如下所示。 除实施例的人工性质,像一个提示Thread.yield()通常是必需的。 在任何情况下,请考虑SwingWorker此处显示为执行演示而执行类似的紧密循环。

尽管测试用例存在人为性质,但我并不认为在这种情况下需要调用Thread.yield()

正确; 屈服只是暴露了潜在的问题: t使事件调度线程变得饥饿 请注意,即使没有Thread.yield() ,GUI也会在下面的示例中立即更新。 如本相关问答中所述 ,您可以尝试降低线程的优先级。 或者,运行t在一个单独的JVM中,所建议的在这里使用ProcessBuilder ,这也可以在一个的后台运行SwingWorker ,如图这里

为什么呢?

所有支持的平台都基于单线程图形库构建。 阻塞,饿死或饱和管理事件派遣线程相当容易。 非平凡的后台任务通常会隐式产生,就像发布中间结果,阻塞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);
    }
}

我的观察:

  • 用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线程。 老实说,我不确定这一切意味着什么,但我相信有人会弄明白

  • 默认情况下,一切都是从EDT(在这种情况下在ActionListener )开始,由Thread.sleep锁定,当所有代码执行时都会结束,包括Thread.sleep ,并假设您丢失所有事件包括。 绘画,在整个时间内完成这个代码,所有事件都画在最后和一次

  • 这段代码丢失了autoclose JOptionPane ,永远不会被绘制到屏幕上(模拟如何在Swing中使用Thread.sleep杀死绘画)

  • Swing GUI对鼠标或键事件不负责任,不可能终止这个应用程序,这可能只是从Runnable#Thread SwingWorkerSwingWorker ,那被指定为启动新的,另一个线程( Workers Thread ),在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)

再次autoclose 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 SwingWorkerSwingWorker进行测试

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

我试图最小化代码,以消除sleepactionPerformed潜在陷阱,我相信我已经这样做,同时保持问题的完整性:

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除非也许你是对一些superduper处理器,可以做的更快的计算,?)

只有第一pack注释掉了:

a [pause]
c-1863004573
b

(不期望)

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

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

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

在Windows上

在步骤2和步骤3之间观察长时间延迟。

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

在Linux上

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM