With the help of people on stackoverflow I was able to get the following working code of a simple GUI countdown (which just displays a window counting down seconds). My main problem with this code is the invokeLater
stuff.
As far as I understand invokeLater
, it sends a task to the event dispatching thread (EDT) and then the EDT executes this task whenever it "can" (whatever that means). Is that right?
To my understanding, the code works like this:
In the
main
method we useinvokeLater
to show the window (showGUI
method). In other words, the code displaying the window will be executed in the EDT.In the
main
method we also start thecounter
and the counter (by construction) is executed in another thread (so it is not in the event dispatching thread). Right?The
counter
is executed in a separate thread and periodically it callsupdateGUI
.updateGUI
is supposed to update the GUI. And the GUI is working in the EDT. So,updateGUI
should also be executed in the EDT. It is the reason why the code for theupdateGUI
is enclosed ininvokeLater
. Is that right?
What is not clear to me is why we call the counter
from the EDT. Anyway, it is not executed in the EDT. It starts immediately, a new thread and the counter
is executed there. So, why can we not call the counter
in the main method after the invokeLater
block?
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
public class CountdownNew {
static JLabel label;
// Method which defines the appearance of the window.
public static void showGUI() {
JFrame frame = new JFrame("Simple Countdown");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
label = new JLabel("Some Text");
frame.add(label);
frame.pack();
frame.setVisible(true);
}
// Define a new thread in which the countdown is counting down.
public static Thread counter = new Thread() {
public void run() {
for (int i=10; i>0; i=i-1) {
updateGUI(i,label);
try {Thread.sleep(1000);} catch(InterruptedException e) {};
}
}
};
// A method which updates GUI (sets a new value of JLabel).
private static void updateGUI(final int i, final JLabel label) {
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
label.setText("You have " + i + " seconds.");
}
}
);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
showGUI();
counter.start();
}
});
}
}
If I understand your question correctly you're wonder why you can't do this:
The reason why you can't do it is because the scheduler makes no guarantees... just because you invoked
showGUI()
and then you invokedcounter.start()
doesn't mean that the code inshowGUI()
will be executed before the code in the run method of thecounter
.Think of it this way:
starts a thread and that thread isschedules an asynchronous event on the EDT which is tasked with creating theJLabel
.JLabel
to exists so it can calllabel.setText("You have " + i + " seconds.");
Now you have a race condition:
JLabel
must be created BEFORE thecounter
thread starts, if it's not created before the counter thread starts, then your counter thread will be callingsetText
on an uninitialized object.In order to ensure that the race condition is eliminated we must guarantee the order of execution and one way to guarantee it is to execute
showGUI()
andcounter.start()
sequentially on the same thread:Now
showGUI();
andcounter.start();
are executed from the same thread, thus theJLabel
will be created before thecounter
is started.Update:
This is simple, it is as follows
Step 1 . Initial thread also called main thread is created.
Step 2. Create a runnable object and pass it to invokeLate().
Step 3. This initialises the GUI but does not create the GUI.
Step 4. InvokeLater() schedules the created object for execution on EDT.
Step 5. GUI has been created.
Step 6. All events occurring will be placed in EDT.
You are actually starting the
counter
thread from the EDT. If you calledcounter.start()
after theinvokeLater
block, the counter would likely start to run before the GUI becomes visible. Now, because you're constructing the GUI in the EDT, the GUI wouldn't exist when thecounter
starts to update it. Luckily you seem to be forwarding the GUI updates to the EDT, which is correct, and since the EventQueue is a queue, the first update will happen after the GUI has been constructed, so there should be no reason why this wouldn't work. But what's the point of updating a GUI that may not be visible yet?It's a hacky workaround around the great many concurrency issues that the Swing API has ;)
Seriously, a lot of Swing components are not "thread safe" (some famous programmers went as far as calling Swing "thread hostile"). By having a unique thread where all updates are made to this thread-hostile components you're dodging a lot of potential concurrency issues. In addition to that, you're also guaranteed that it shall run the
Runnable
that you pass through it usinginvokeLater
in a sequential order.Then some nitpicking:
And then:
You don't really start the counter in the main method. You start the counter in the run() method of the anonymous
Runnable
that is executed on the EDT. So you really start the counterThread
from the EDT, not the main method. Then, because it's a separate Thread, it is not run on the EDT. But the counter definitely is started on the EDT, not in theThread
executing themain(...)
method.It's nitpicking but still important seen the question I think.