[英]Busy loop in other thread delays EDT processing
我有一个 Java 程序,它在一个单独的(非 EDT)线程上执行一个紧密循环。 虽然我认为 Swing UI 应该仍然是响应式的,但事实并非如此。 下面的示例程序显示了这个问题:单击“试试我”按钮应该会在半秒后或多或少地弹出一个对话框,并且应该可以通过单击它的任何响应来立即关闭该对话框。 相反,对话框需要更长的时间才能出现,和/或在单击其中一个按钮后需要很长时间才能关闭。
有谁知道为什么 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循环中做一些工作:
使用此代码,可以观察到预期的行为:
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());
});
...
});
我在一台半功能的机器上运行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
SwingWorker
和SwingWorker
,那被指定为启动新的,另一个线程( 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
SwingWorker
和SwingWorker
进行测试
这不是最终答案,但它更接近于理解问题。
我试图最小化代码,以消除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
除非也许你是对一些superduper处理器,可以做的更快的计算,?)
只有第一pack
注释掉了:
a [pause]
c-1863004573
b
(不期望)
你能用-client
标志确认我的结果吗?
这似乎是一个问题。 下面是观察Event Dispatcher Thread延迟处理,应该立即响应:
在Windows上
在步骤2和步骤3之间观察长时间延迟。
第3步 - >立即关闭对话框。
在Linux上
第2步到第3步 - 没有延迟。
第3步 - >长时间关闭对话框。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.