【问题标题】:How to start/resume and stop/pause a thread inside the action listener in java如何在java中的动作侦听器中启动/恢复和停止/暂停线程
【发布时间】:2025-11-28 20:10:01
【问题描述】:

我正在用java写一个简单的多线程练习。我需要做的基本上就是制作一个带有两个按钮(“开始”和“结束”)的 JFrame。如果用户点击“开始”按钮,控制台将开始打印出“正在打印”。如果单击“结束”,控制台将停止打印。再次单击“开始”将恢复打印。

这是我的代码(不相关的部分没有显示):

//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;
    }
}

但是,一旦单击开始按钮,控制台就不会停止打印。我不断收到 IllegalMonitorStateException。是什么导致了这个问题? 我找不到错误,因为所有部分似乎在逻辑上都是正确的。任何帮助将不胜感激。

【问题讨论】:

  • 我认为 thr.wait() 和 thr.notify() 是问题所在。但是如果我不使用它们,我用什么来暂停和恢复线程呢?
  • wait() 不会像您认为的那样做。提示:它继承自 Object,并且有一个不错的 javadoc ...
  • IllegalMonitorStateException - 如果当前线程不是对象监视器的所有者这是我不断得到的。

标签: java multithreading swing


【解决方案1】:

提供的代码不会打印任何内容。它也不会编译,您需要将private static Thread; 修复为private static Thread thr;

无论如何,这是否可行,取决于代码缺少任何同步。这意味着在一个线程中对变量所做的更改不需要在另一个线程中可见。如果您最初将单个变量设置为false,然后在一个线程中将其设置为true,则第二个线程仍然可以看到它的缓存值false

尝试让你的boolean 变量volatile 看看它是否有效,但真正的答案是阅读线程同步e.g. in the Java Tutorial

【讨论】:

  • Grammarly 的自动更正...它将“thr”更改为“the”,然后我删除了“the”。 :(
【解决方案2】:

thr.wait() 调用将执行以下操作:

  1. 暂停调用该方法的Thread
  2. 释放Thread(调用方法)当前持有的所有锁。

对应的notify(或notifyAll)方法调用应与暂停我们想要继续的Thread的完全相同的对象(即thr.notify()thr.notifyAll())进行。

请注意,Event Dispatch Thread(简称 EDT)(它本身就是 Thread)上调用了动作监听器 actionPerformed 方法。也就是说,通过单击end 按钮,在EDT 上调用actionPerformed,然后在其上调用thr.wait(),这意味着您暂停了EDT!在 Swing 中,据我所知,几乎所有与事件相关的操作都发生在 EDT 上。这意味着,如果您在 EDT 上运行,那么您会阻止其他操作,例如接收来自按钮单击、鼠标移动和悬停等的事件……总之,阻止 EDT 意味着 GUI 无响应。

除此之外,thr.wait() 调用(以及thr.notify()thr.notifyAll())应该在synchronized (thr) { ... } 块内完成。

如果您想与不同于 EDT 的 Thread 进行交互(例如通过使用 Thread 构造函数、ExecutorServiceSwingWorker 等...),并在两个Threads,您通常需要某种同步(因为您有两个Threads:EDT 和创建的那个)。您将需要这种同步,因为两个 Threads(为了通信)将共享 [a reference to] 同一个变量。在您的情况下,需要共享 print 标志;一个Thread(EDT)应根据按下的按钮修改标志,而另一个Thread(由Example的实例构成,即Runnable)命名为thr , 应在一定间隔/时间后重复读取标志,然后在System.out 中进行打印工作。

还要注意,print 标志是 Example 类的静态属性,但您需要一个类实例以使 Threads 同步。因此,您似乎打算为此使用名为 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) { //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);
    }
}

您可以在此处看到,我创建了一个自定义 Thread 并覆盖了 run 方法,以便在 500 毫秒的间隔/时间后在 System.out 上重复打印。循环永远不会结束,除非Thread 被中断。但不要用作您正在尝试的一个很好的示例实现,因为:

  1. 它不具备正常终止Thread 的条件。例如,它应该在while 循环中有一个条件而不是true,以指示我们何时需要优雅地退出Thread
  2. 它在循环中调用Thread.sleep。据我所知,这被认为是不好的做法,因为这通常是当您需要重复执行操作并依靠Thread.sleep 给您一些空闲时间时,您应该使用ScheduledExecutorServicejava.util.Timer 以固定速率安排所需的操作。

还请注意,您需要在此处进行同步,因为您有两个 Threads(EDT 和 PrintingThread)。我再说一遍,因为在下一个示例中,我们将简单地利用 EDT 本身进行打印(因为在这种情况下,在 System.out 中打印一条消息不会太长),这是另一个示例实施您正在尝试做的事情。为了在 EDT 本身上以固定速率安排操作,我们将使用为此目的而存在的 javax.swing.Timer

代码:

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);
    }
}

javax.swing.Timer 代表循环的目的。

还要注意这里,我们没有使用 synchornized 关键字,因为我们不需要,因为所有代码都在 EDT 上运行。

SwingUtilities.invokeLater 只是将来某个时候在 EDT 上调用 Runnable 的少数方法。所以我们还需要在EDT上调用JFrameJPanelJRadioButtons(或者干脆调用createAndShowGUI)的创建,因为它是EDT相关的代码(例如如果一个事件怎么办?将面板添加到框架时被触发?...)。

我在代码中添加了一些 cmets,以帮助解决与所示示例相关的其他内容。

如果有任何问题,请在 cmets 中告诉我,我会尽快更新答案。

【讨论】:

  • 很高兴它有帮助。 :)