I am writing a simple multithread practice with java. All I need to do is basically make a JFrame with two buttons ("start" and "end"). If the user clicks the "start" button, the console will start printing out "Printing". And if "end" is clicked, the console will stop printing. Clicking "start" again will resume the printing.
Here is my code(irrelevant parts are not shown):
//import not shown
public class Example extends JFrame implements Runnable {
private static boolean print, started;//print tells whether the thread should keep printing
//things out, started tells whether the thread has been
//started
private JButton start;//start button
private JButton end;//end button
private static Thread thr;//the thread that is going to do the printing
//other fields not shown
public Example(String title) {
Container c = getContentPane();
//set up the JFrame
//...parts not shown
start = new JButton("Start");
end = new JButton("End");
c.add(start);
c.add(end);
//add the actionListner for the buttons
start.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (started == false) {
thr.start();// if the thread has not been started, start the thread
started = true;
}else{//otherwise waken the thread. This is to prevent IllegalThreadStateException.
thr.notify();
}
print = true;//should print things out
}
});
end.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(started) {//This action won't pause the thread if its not yet started
try {
thr.wait();//pause the thread
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
print = false;//should stop printing
}
});
validate();
setVisible(true);
}
@Override
public void run() {//override run() method
while (print) {
System.out.println("Printing");//print out "Printing"
}
}
public static void main(String[] args) {//main method
Example ex = new Example("My Frame");//instantiate the frame
thr = new Thread(ex);//make a new thread;
started = false;//the thread has not been started, so set started to false;
}
}
However, once the start button is clicked, the console never stops printing. I keep getting IllegalMonitorStateException. What is causing this issue? I couldn't find the mistake, as all parts seem to be logically correct. Any help will be appreciated.
The code, as provided will not print anything. It also won't compile, you need to fix private static Thread;
to say private static Thread thr;
.
Anyway, this can work or not, depending, as the code lacks any synchronization. This means changes made to a variable in one thread need not be visible in another. If you have a single variable set to false
initially, and then set it to true
in one thread, a second thread can still see it's cached value of false
.
Try making your boolean
variables volatile
and see if it works, but a real answer is reading up on thread synchronization e.g. in the Java Tutorial
The thr.wait()
call will do the following:
Thread
that called the method!Thread
(that called the method) currently holds.The corresponding notify
(or notifyAll
) method call should be made for the exact same object (ie thr.notify()
or thr.notifyAll()
) that suspended the Thread
we want to continue.
Notice that the action listener actionPerformed
method is called on the Event Dispatch Thread (EDT for short) (which is itself a Thread
). That is, by clicking the end
button, the actionPerformed
is called on the EDT and then you call thr.wait()
on it, which means that you suspend the EDT! In Swing, as far as I know, almost every event related operation takes place on the EDT. That means that if you run on the EDT then you block other operations, such as receiving events from button clicks, mouse movement and hovering, etc... In short, blocking the EDT means unresponsive GUI.
Aside from that, thr.wait()
call (as well as thr.notify()
and thr.notifyAll()
) should be done inside a synchronized (thr) { ... }
block.
If you want to interact with a Thread
different than the EDT (such as by using the Thread
constructors, an ExecutorService
, a SwingWorker
etc...), and also make a communication between the two Thread
s, you usually need some kind of synchronization (because you have two Thread
s: the EDT and the one created). You will need this synchronization because the two Thread
s (in order to communicate) are going to share [a reference to] the same variable. In your case it's the print
flag which needs to be shared; one Thread
(the EDT) shall modify the flag, according to what button was pressed, while the other Thread
(the one constructed with an instance of the class Example
which is the Runnable
) named thr
, shall read the flag repeatedly after some interval/time and then do the work of printing in System.out
.
Notice also, that the print
flag is a static property of the class Example
, but you need a class instance for the Thread
s to synchornize on. So it seems like you were going to use the Example
class instance named thr
for this.
Take for example the following code:
import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.SwingUtilities;
public class ThreadMain {
private static class PrintingThread extends Thread {
private boolean print;
public PrintingThread() {
print = false;
}
public synchronized void keepPrinting() {
print = true;
notifyAll();
}
public synchronized void pausePrinting() {
print = false;
}
@Override
public void run() {
try {
while (true) { //You should add an end condition here, in order to let the Thread shutdown gracefully (other than interrupting it).
synchronized (this) {
if (!print)
wait();
}
System.out.println("Printing...");
Thread.sleep(500);
}
}
catch (final InterruptedException ix) {
System.out.println("Printing interrupted.");
}
}
}
private static void createAndShowGUI() {
final PrintingThread printingThread = new PrintingThread();
printingThread.start();
final JRadioButton start = new JRadioButton("Print"),
stop = new JRadioButton("Pause", true);
start.addActionListener(e -> printingThread.keepPrinting());
stop.addActionListener(e -> printingThread.pausePrinting());
/*Creating a button group and adding the two JRadioButtons, means that when
you select the one of them, the other is going to be unselected automatically.
The ButtonGroup instance is then going to be maintained in the model of each
one of the buttons (JRadioButtons) that belong to the group, so you don't need
to keep a reference to group explicitly in case you worry it will get Garbadge
Collected, because it won't.*/
final ButtonGroup group = new ButtonGroup();
group.add(start);
group.add(stop);
final JPanel contentsPanel = new JPanel(); //FlowLayout by default.
contentsPanel.add(start);
contentsPanel.add(stop);
final JFrame frame = new JFrame("Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(contentsPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(final String[] args) {
//EDT related code should be called on the EDT..
SwingUtilities.invokeLater(ThreadMain::createAndShowGUI);
}
}
You can see here that I created a custom Thread
and overrided run
method to repeatedly print on System.out
after some interval/time of 500ms. The loop will never end, unless the Thread
is interrupted. Not to be used as a good example implementation of what you are trying though, because:
Thread
. It should have for example a condition instead of true
in the while
loop to indicate when we are needed to exit the Thread
gracefully.Thread.sleep
in the loop. This is considered bad practice as far as I know, because this is the case usually when you need to do an operation repeatedly and rely on Thread.sleep
to give you some spare time, when instead you should have used a ScheduledExecutorService
or a java.util.Timer
to schedule at fixed rate the desired operation.Also note that you need synchornization here because you have two Thread
s (the EDT and the PrintingThread
). I'm saying this again because in the next example we are going to simply utilize the EDT itself to do the printing (because printing in System.out
a single message is not going to be too long in this case), which is another sample implementation of what you are trying to do. To schedule the operation at a fixed rate on the EDT itself, we are going to use the javax.swing.Timer
which exists for such a purpose.
The code:
import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class TimerMain {
private static void createAndShowGUI() {
//Constructs a Timer such that, when running, every 500ms prints the desired message:
final Timer printingTimer = new Timer(500, e -> System.out.println("Printing..."));
/*The Timer is going to repeat events (ie call all its
ActionListeners repeatedly)... This will simulate a loop.*/
printingTimer.setRepeats(true);
/*Coalescing means that events fast enough are going to be merged to one
event only, and we don't want that in this case, so we set it to false:*/
printingTimer.setCoalesce(false);
final JRadioButton start = new JRadioButton("Print"),
stop = new JRadioButton("Pause", true);
start.addActionListener(e -> printingTimer.restart());
stop.addActionListener(e -> printingTimer.stop());
/*Creating a button group and adding the two JRadioButtons, means that when
you select the one of them, the other is going to be unselected automatically.
The ButtonGroup instance is then going to be maintained in the model of each
one of the buttons (JRadioButtons) that belong to the group, so you don't need
to keep a reference to group explicitly in case you worry it will get Garbadge
Collected, because it won't.*/
final ButtonGroup group = new ButtonGroup();
group.add(start);
group.add(stop);
final JPanel contentsPanel = new JPanel(); //FlowLayout by default.
contentsPanel.add(start);
contentsPanel.add(stop);
final JFrame frame = new JFrame("Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(contentsPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(final String[] args) {
//EDT related code should be called on the EDT...
SwingUtilities.invokeLater(TimerMain::createAndShowGUI);
}
}
The javax.swing.Timer
delegates the purpose of the loop.
Also notice here, we didn't use the synchornized
keyword, because we didn't need to, because all the code runs on the EDT.
SwingUtilities.invokeLater
is just a handful method to invoke a Runnable
on the EDT at some point in the future. So we also need to invoke the creation of the JFrame
, the JPanel
and the JRadioButton
s (or simply call the createAndShowGUI
) on the EDT, because it is EDT related code (for example what if an event was fired while adding the panel to the frame?...).
I added some comments in the code to help out for other stuff related to the examples shown.
Let me know in the comments any questions that may arise, and I will update my answer as soon as possible.
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