Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 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.

like image 288
Cloud Walker Avatar asked Aug 01 '20 21:08

Cloud Walker


2 Answers

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

like image 182
pafau k. Avatar answered Nov 15 '22 18:11

pafau k.


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.

like image 36
gthanop Avatar answered Nov 15 '22 17:11

gthanop