如何在Java的动作监听器中启动/恢复和停止/暂停线程。

huangapple go评论78阅读模式
英文:

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:

  1. 暂停调用该方法的 Thread
  2. 释放调用该方法的 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 构造函数、ExecutorServiceSwingWorker 等),并且还要在这两个 Thread 之间进行通信,通常需要一些类型的同步(因为两个 Thread 需要共享[对]相同变量的引用)。在你的情况下,这个变量是需要共享的 print 标志;一个 Thread(EDT)应根据按下的按钮来修改标志,而另一个 Thread(通过创建一个 Example 类的实例构造的 thr)应在一段时间后重复地读取该标志,然后在 System.out 中执行打印工作。

还要注意,print 标志是 Example 类的静态属性,但你需要一个类实例来对 Thread 进行同步。所以看起来你打算使用名为 thrExample 类实例来进行这种同步。

例如,考虑以下代码:

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。不过请注意,这并不是一个良好的示例实现你所要做的事情,因为:

  1. 它没有正常终止线程的条件。例如,循环应该具有结束条件,以指示我们何时需要正常退出线程,而不仅仅是中断它。
  2. 它在循环中调用了 Thread.sleep。据我所知,这被认为是不良实践,因为这通常是在需要重复执行操作并依赖于 Thread.sleep 以提供一些空闲时间时使用的情况,而相反,你应该使用 ScheduledExecutorServicejava.util.Timer 来按固定速率调度所需的操作。

此外,注意到这里需要同步,因为你有两个 Thread(EDT 和 PrintingThread)。我再次提到这一点,因为在下一个示例中,我们将简单地利用 EDT 本身来执行打印(因为在这种情况下,向 System.out 打印单个消息不会太长),这是你要做的另一个示例实现。为了在 EDT 上本身按固定速率调度操作

英文:

The thr.wait() call will do the following:

  1. Suspend the Thread that called the method!
  2. 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 Threads, you usually need some kind of synchronization (because you have two Threads: the EDT and the one created). You will need this synchronization because the two Threads (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 Threads 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:

  1. It doesn't have a condition for normal termination of the 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.
  2. 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 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 Threads (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 JRadioButtons (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.

huangapple
  • 本文由 发表于 2020年8月2日 05:31:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/63210268.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定