javamultithreadingconcurrencyeventqueueedt

Stopping another thread from the EDT (EventQueue)


So I created a basic swing interface with a stop button, and clicking it should stop a counting thread. When the application starts, a thread instance would allocate a runnable class which does a counting loop and logs it in the console. There is a method in the runnable interface that sets the volatile variable to false that should basically stop the thread and I called it on the stop button, but why doesn't it stop the loop? Here is my code.

ParentContainer.java

public class ParentContainer extends JFrame implements ActionListener, WindowListener {
    private static final long serialVersionUID = 1L;
    private JButton btnStop;

    private static CountRunnable cr;

    public ParentContainer() {
        Container cp = getContentPane();
        cp.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
        btnStop = new JButton("Stop Counting");
        cp.add(btnStop);

        SymAction lSymAction = new SymAction();
        btnStop.addActionListener(lSymAction);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Counter");
        setSize(200, 90);
        setVisible(true);
    }

    public static void main(String[] args) {
        // Run GUI codes in Event-Dispatching thread for thread safety
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new ParentContainer(); // Let the constructor do the job
            }
        });

        cr = new CountRunnable();
        Thread t1 = new Thread(cr, "MyRunnable");
        t1.start();
    }

    class SymAction implements ActionListener
    {
        public void actionPerformed(ActionEvent evt)
        {
            Object object = evt.getSource();
            if (object == btnStop) { btnStop_actionPerformed(evt); }
        }
    }

    void btnStop_actionPerformed(ActionEvent evt)
    {
        cr.stop();
    }

    @Override
    public void windowActivated(WindowEvent arg0) { }

    @Override
    public void windowClosed(WindowEvent arg0) { }

    @Override
    public void windowClosing(WindowEvent arg0) { }

    @Override
    public void windowDeactivated(WindowEvent arg0) { }

    @Override
    public void windowDeiconified(WindowEvent arg0) { }

    @Override
    public void windowIconified(WindowEvent arg0) { }

    @Override
    public void windowOpened(WindowEvent arg0) { }

    @Override
    public void actionPerformed(ActionEvent arg0) { }
}

CountRunnable.java

public class CountRunnable implements Runnable {
    public static volatile boolean keepRunning;
    private int count = 1;

    @Override
    public void run() {
        keepRunning = true;
        while (keepRunning)
        {
            for (int i = 0; i < 100000; ++i) {
                System.out.println(count + "");
                ++count;
                // Suspend this thread via sleep() and yield control to other threads.
                // Also provide the necessary delay.
                try {
                    Thread.sleep(10); // milliseconds
                }
                catch (InterruptedException ex) {
                }
            }
        }
    }

    public void stop()
    {
        keepRunning = false;
    }
}

Solution

  • When making a stop condition, it is important that you check the condition frequently. At the moment, you have a loop that runs for a while, and you only see the result of your check at the end of the loop.

    To make sure the loop ends mid-iteration, you can add the following check inside the loop.

    if(!keepRunning) {
        break;
    }
    

    There are different solutions you can also try. Because this pattern of stopping threads is frequently used, you can also use a SwingWorker for your sub task this class gives a fair amount of useful utility methods for stopping, and for updating the gui thread safe from your subtask (so for example, you can show count in the gui instead of the command line)

    Changing your code to use a SwingWorkeris easy, when extending SwingWorker, it expects 2 elements, a "final result", and a "pending result".

    Example of SwingWorker:

    SwingWorker<String, String> task = new SwingWorker<String, String>(){
        private int count = 1;
    
        @Override
        public List<Integer> doInBackground() throws Exception {
            while (!isCancelled()) {
                for (int i = 0; i < 100000; ++i) {
                    publish(count + "");
                    ++count;
                    Thread.sleep(10); // milliseconds
                }
            }
            return count + "";
        }
    
        @Override
        protected void process(List<Integer> chunks) {
            gui.setText(chunk.get(chunk.size() - 1));
        }
    
        @Override
        protected void done() {
            try {
                gui.setText(this.get());
            } catch(InterruptedException | ExecutionException ex) {
                ex.printSTackTrace();
            }
        }
    }
    task.execute();
    
    // Later:
    
    task.cancel(false);
    

    Notice that canceling with false is recommended, because when doing it with true means that the thread executing the counter will be interrupted, causing the Thread.sleep() to throw an exception.

    If the purpose of your task is only to count a number, it may be even easier to use a timer (Make sure you import the correct one) class instead, using the class is as easy as:

    int delay = 10; //milliseconds
    ActionListener taskPerformer = new ActionListener() {
        int count = 0;
        public void actionPerformed(ActionEvent evt) {
            count++;
            gui.setText(count + "");
            // System.out.println(count + "");
        }
    };
    Timer task = new Timer(delay, taskPerformer);
    task.setCoalesce(false); // SOmetimes, executions can be missed because other programs 
    task.setRepeating(true);
    task.start();
    
    // Later:
    task.stop();
    

    Notice that with this timer solution, you can restart it again by calling start(), unlike the other solutions I shown you.