英文:
How to start/resume and stop/pause a thread inside the action listener in java
问题
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 actionListener 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.
英文:
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.
答案1
得分: 4
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 its 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 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
答案2
得分: 3
The thr.wait()
call will do the following:
- 暂停调用该方法的
Thread
! - 释放调用该方法的
Thread
(当前持有的)任何锁。
对应的 notify
(或 notifyAll
)方法调用应该针对完全相同的对象进行(即 thr.notify()
或 thr.notifyAll()
),该对象暂停了我们想要继续的 Thread
。
请注意,动作监听器 actionPerformed
方法在 事件分派线程(EDT,简称)上调用(它本身也是一个 Thread
)。也就是说,通过点击 end
按钮,actionPerformed
在 EDT 上调用,然后你在其上调用 thr.wait()
,这意味着你暂停了 EDT!在 Swing 中,据我所知,几乎每个与事件相关的操作都在 EDT 上进行。这意味着如果你在 EDT 上运行,那么你会阻塞其他操作,比如接收按钮点击、鼠标移动和悬停等事件... 简而言之,阻塞 EDT 意味着不响应的 GUI。
除此之外,thr.wait()
调用(以及 thr.notify()
和 thr.notifyAll()
)应该在 synchronized (thr) { ... }
块内完成。
如果你想与不同于 EDT 的 Thread
交互(例如使用 Thread
构造函数、ExecutorService
、SwingWorker
等),并且还要在这两个 Thread
之间进行通信,通常需要一些类型的同步(因为两个 Thread
需要共享[对]相同变量的引用)。在你的情况下,这个变量是需要共享的 print
标志;一个 Thread
(EDT)应根据按下的按钮来修改标志,而另一个 Thread
(通过创建一个 Example
类的实例构造的 thr
)应在一段时间后重复地读取该标志,然后在 System.out
中执行打印工作。
还要注意,print
标志是 Example
类的静态属性,但你需要一个类实例来对 Thread
进行同步。所以看起来你打算使用名为 thr
的 Example
类实例来进行这种同步。
例如,考虑以下代码:
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) { // 你应该在这里添加一个结束条件,以使线程正常关闭(而不仅仅是中断它)。
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());
/* 创建一个按钮组并添加两个 JRadioButton,这意味着当你选择其中一个时,另一个会自动取消选择。
ButtonGroup 实例将在属于组的每个按钮(JRadioButton)的模型中维护,因此你不需要显式保留对组的引用,以防它被垃圾回收,因为它不会被回收。*/
final ButtonGroup group = new ButtonGroup();
group.add(start);
group.add(stop);
final JPanel contentsPanel = new JPanel(); // 默认为 FlowLayout。
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 相关的代码应该在 EDT 上调用...
SwingUtilities.invokeLater(ThreadMain::createAndShowGUI);
}
}
在这里,我创建了一个自定义的 Thread
,并重写了 run
方法,以便在 500ms 的一段时间后重复在 System.out
上打印。循环永远不会结束,除非中断了这个 Thread
。不过请注意,这并不是一个良好的示例实现你所要做的事情,因为:
- 它没有正常终止线程的条件。例如,循环应该具有结束条件,以指示我们何时需要正常退出线程,而不仅仅是中断它。
- 它在循环中调用了
Thread.sleep
。据我所知,这被认为是不良实践,因为这通常是在需要重复执行操作并依赖于Thread.sleep
以提供一些空闲时间时使用的情况,而相反,你应该使用ScheduledExecutorService
或java.util.Timer
来按固定速率调度所需的操作。
此外,注意到这里需要同步,因为你有两个 Thread
(EDT 和 PrintingThread
)。我再次提到这一点,因为在下一个示例中,我们将简单地利用 EDT 本身来执行打印(因为在这种情况下,向 System.out
打印单个消息不会太长),这是你要做的另一个示例实现。为了在 EDT 上本身按固定速率调度操作
英文:
The thr.wait()
call will do the following:
- Suspend the
Thread
that called the method! - Release any locks the
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:
- It doesn't have a condition for normal termination of the
Thread
. It should have for example a condition instead oftrue
in thewhile
loop to indicate when we are needed to exit theThread
gracefully. - It calls
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 onThread.sleep
to give you some spare time, when instead you should have used aScheduledExecutorService
or ajava.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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论