I have a Java program which executes a tight loop on a separate (non-EDT) thread. Although I would think the Swing UI should still be responsive, it is not. The example program below exhibits the problem: clicking the "Try me" button should pop up a dialog more-or-less half a second later, and it should be possible to close that dialog immediately by clicking any of its responses. Instead, the dialog takes much longer to appear, and/or takes a long time to close after clicking one of the buttons.
Does anyone have any idea why the EDT processing is being delayed, even though there is only a single busy thread?
(Please note that despite various suggestions of the Thread.sleep
call being the cause of the problem, it is not. It can be removed and the problem can still be reproduced, though it manifests slightly less frequently and usually exhibits the second behavior described above - i.e. non-responsive JOptionPane
dialog rather than delayed dialog appearance. Furthermore, there is no reason that the sleep call should yield to the other thread because there are spare processor cores as mentioned above; the EDT could continue to run on another core after the call to 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);
}
}
Update: The problem occurs only with the server VM (but see further update). Specifying the client VM (-client
command line argument to java executable) seems to suppress the problem (update 2) on one machine but not another.
Update 3: I see 200% processor usage by the Java process after clicking the button, implying that there are 2 processor cores fully loaded. This doesn't make sense to me at all.
Update 4: Also occurs on Windows.
Update 5: Using a debugger (Eclipse) proves problematic; the debugger seems unable to stop the threads. This is highly unusual, and I suspect there is some sort of livelock or race condition in the VM, so I've filed a bug with Oracle (review ID JI-9029194).
Update 6: I found my bug report in the OpenJDK bug database. (I was not informed that it had been accepted, I had to search for it). The discussion there is most interesting and already sheds some light on what may be the cause of this problem.
You can use invokeAndWait() and invokeLater() to update a Swing component from any arbitrary thread.
Thread class provides the join() method which allows one thread to wait until another thread completes its execution.
4.2. On Windows machines, there's no limit specified for threads. Thus, we can create as many threads as we want, until our system runs out of available system memory.
I see the same effect to Mac OS X. Although your example is correctly synchronized, the platform/JVM variability you see is likely due to vagaries in how threads are scheduled, resulting is starvation. Adding Thread.yield()
to the outer loop in t
mitigates the problem, as shown below. Except for the artificial nature of the example, a hint like Thread.yield()
would not ordinarily be required. In any case, consider SwingWorker
, shown here executing a similarly tight loop for demonstration purposes.
I do not believe that
Thread.yield()
should need to be called in this case at all, despite the artificial nature of the test case, however.
Correct; yielding simply exposes the underlying problem: t
starves the event dispatch thread. Note that the GUI updates promptly in the example below, even without Thread.yield()
. As discussed in this related Q&A, you can try lowering the thread's priority. Alternatively, run t
in a separate JVM, as suggested here using ProcessBuilder
, which can also run in the background of a SwingWorker
, as shown here.
but why?
All supported platforms are built on single-threaded graphics libraries. It's fairly easy to block, starve or saturate the governing event dispatch thread. Non-trivial background tasks typically yield implicitly, as when publishing intermediate results, blocking for I/O or waiting on a work queue. A task that does not do may have to yield explicitly.
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);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With