其他线程中的繁忙循环延迟EDT处理
我有一个Java程序,在一个单独的(非EDT)线程上执行一个严格的循环。 尽pipe我认为Swing UI仍然可以响应,但事实并非如此。 下面的示例程序显示了这个问题:单击“尝试我”button应该会在半秒钟后或多或less地popup一个对话框,并且应该可以通过单击任何响应来立即closures该对话框。 相反,对话框需要更长的时间才能显示,并且/或者点击其中一个button需要很长时间才能closures。
- 问题发生在Linux(两个不同的发行版),Windows,Raspberry Pi(仅限服务器VM)和Mac OS X(由另一个SO用户报告)上。
- Java版本1.8.0_65和1.8.0_72(都尝试过)
- i7处理器拥有多个内核。 EDT应该有足够的备用处理能力。
有没有人知道为什么EDT处理被延迟,即使只有一个繁忙的线程?
(请注意,虽然Thread.sleep
调用的各种build议是造成问题的原因,但它不是,它可以被删除,问题仍然可以被复制,尽pipe它显示的频率稍低,并且通常performance出上述第二种行为- 即没有响应的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(对java可执行文件的客户端命令行参数)似乎在一台机器上抑制了问题( 更新2 ) ,而不是另一台 。
更新3:点击button后,Java进程看到200%的处理器使用率,意味着有2个处理器内核已经完全加载。 这对我来说根本没有意义。
更新4:也发生在Windows上。
更新5:使用debugging器(Eclipse)certificate有问题; debugging器似乎无法停止线程。 这是非常不寻常的,我怀疑虚拟机中存在某种活锁或竞争状态,所以我提交了Oracle的一个错误(评论ID JI-9029194)。
更新6:我在OpenJDK错误数据库中发现了我的错误报告 。 (我没有被通知它已被接受,我不得不寻找它)。 这里的讨论最有意思,并且已经阐明了这个问题的原因。
我看到了与Mac OS X相同的效果。虽然您的示例已正确同步,但是您看到的平台/ JVM可变性可能是由于计划中的线程变幻莫测,导致了饥饿。 将Thread.yield()
添加到t
的外部循环中可缓解问题,如下所示。 除了这个例子的人为特征之外, Thread.yield()
这样的提示通常是不需要的。 在任何情况下,考虑SwingWorker
, 这里显示执行一个类似的严格循环出于演示的目的。
我不相信
Thread.yield()
在这种情况下根本就不需要被调用,尽pipe这个testing用例是人为的。
正确; 产生简单的暴露底层的问题: t
饿死事件调度线程。 请注意,即使没有Thread.yield()
,GUI在下面的示例中也会立即更新。 正如在这个相关的Q&A中所讨论的那样,您可以尝试降低线程的优先级。 或者,在单独的JVM中运行t
,如在此处使用ProcessBuilder
所build议的,也可以在SwingWorker
的后台运行,如此处所示。
但为什么 ?
所有支持的平台都build立在单线程graphics库上。 阻止,扼杀或饱和pipe理事件调度线程是相当容易的。 非平凡的后台任务通常隐含地产生,就像发布中间结果,阻塞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); } }
我的观察:
-
用swingworkerreplace线程:
- 没有不同
-
用swingworkerreplace线程,并在第一个for循环内部做一些工作:
- 得到了不冻结的预期结果,从这里开始一帆风顺
有了这个代码,就可以看到预期的行为:
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的原始代码:
- 在第一个循环内做一些其他的工作
- 得到预期的结果
例
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()); }); ... });
我在一个半function的机器上运行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线程。 老实说,我不知道这是什么意思,但我相信有人会弄清楚
-
默认情况下,所有从
Thread.sleep
locking的EDT(本例中为ActionListener
内部)开始的所有代码都被执行,包括Thread.sleep
,并假设您丢失了所有事件。 绘画,在整个过程中encryption这个代码,所有的事件都画在最后一次 -
这段代码丢失了自动闭合的
JOptionPane
,从来没有被绘制到屏幕上(模拟如何安装Thread.sleep
杀死Swing中的绘画) -
Swing GUI对于鼠标或键盘事件是不负责任的,不可能终止这个应用程序,这可能仅仅是从
Runnable#Thread
和SwingWorker
,那是指定开始新的,另一个线程(Workers Thread
),在Runnable#Thread
和SwingWorker
是任务可取消(或通过使用Runnable#Thread
有可能暂停,修改…) -
这不是关于multithreading,也不是将资源共享给另一个核心,在Win10中,所有内核都显示给我,按比例分享增量
输出(less许修改)的代码(基于你的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
永远不会被绘制到屏幕上(testingwin10-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
进行testing
这不是最终的答案,但它更接近理解这个问题。
我试图尽量减less代码,以消除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
标志来确认我的结果吗?
这似乎是一个问题。 下面是观察Event Dispatcher Thread的延迟处理,应该立即作出响应:
- 执行示例程序
- 点击“尝试我”button
- 点击任何button(是/否/取消)closures结果对话框
在Windows上
长时间观察第2步和第3步。
第3步 – >立即closures对话框。
在Linux上
步骤2到步骤3 – 不延迟。
步骤3 – >长时间closures对话框。