Как запустить / возобновить и остановить / приостановить поток внутри слушателя действий в java - PullRequest
8 голосов
/ 02 августа 2020

Я пишу простую многопоточную практику с 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. Что вызывает эту проблему? Я не смог найти ошибку, так как все части кажутся логически правильными. Любая помощь будет оценена по достоинству.

Ответы [ 2 ]

3 голосов
/ 02 августа 2020

Вызов thr.wait() будет делать следующее:

  1. Приостановить Thread, который вызвал метод!
  2. Освободить любые блокировки Thread (который вызвал метод ) в настоящее время выполняется.

Соответствующий вызов метода notify (или notifyAll) должен выполняться для того же объекта (ie thr.notify() или thr.notifyAll()), который приостановил Thread мы хотим продолжить.

Обратите внимание, что метод прослушивателя действий actionPerformed вызывается в потоке отправки событий (сокращенно EDT) (который сам по себе является Thread). То есть, нажав кнопку end, вызывается actionPerformed на EDT, а затем вы вызываете thr.wait() на нем, что означает, что вы приостанавливаете EDT! В Swing, насколько мне известно, почти каждая операция, связанная с событием, выполняется на EDT. Это означает, что если вы работаете на EDT, вы блокируете другие операции, такие как получение событий от щелчков кнопок, движения мыши и зависания и т. Д. c ... Короче говоря, блокировка EDT означает отсутствие ответа GUI.

Помимо этого, вызов thr.wait() (а также thr.notify() и thr.notifyAll()) должен выполняться внутри блока synchronized (thr) { ... }.

Если вы хотите взаимодействовать с Thread другим чем EDT (например, с помощью конструкторов Thread, ExecutorService, SwingWorker и c ...), а также установить связь между двумя Thread s, обычно вам нужны какие-то синхронизации (потому что у вас есть два Thread s: EDT и созданный). Вам понадобится эта синхронизация, потому что два Thread s (для связи) будут совместно использовать [ссылку на] одну и ту же переменную. В вашем случае это флаг print, которым нужно поделиться; один Thread (EDT) должен изменять флаг в зависимости от того, какая кнопка была нажата, а другой Thread (созданный с экземпляром класса Example, который является Runnable) с именем thr , должен повторно прочитать флаг через некоторый интервал / время, а затем выполнить работу по печати в System.out.

Обратите внимание, что флаг print является статическим c свойством класса Example, но вам нужен экземпляр класса для Thread s для синхронизации. Похоже, вы собирались использовать для этого экземпляр класса Example с именем thr.

Возьмем, к примеру, следующий код:

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 для повторной печати на System.out через некоторый интервал / время в 500 мс. L oop никогда не закончится, если Thread не будет прервано. Не следует использовать в качестве хорошего примера реализации того, что вы пытаетесь, потому что:

  1. У него нет условия для нормального завершения Thread. Например, у него должно быть условие вместо true в while l oop, чтобы указать, когда нам необходимо корректно выйти из Thread.
  2. Он вызывает Thread.sleep в l oop. Насколько я знаю, это считается плохой практикой, потому что обычно это происходит, когда вам нужно выполнять операцию несколько раз и полагаться на Thread.sleep, чтобы дать вам немного свободного времени, тогда как вместо этого вы должны были использовать ScheduledExecutorService или java.util.Timer, чтобы запланировать желаемую операцию с фиксированной скоростью.

Также обратите внимание, что здесь вам нужна синхронизация, потому что у вас есть два Thread s (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 делегирует назначение l oop.

Также обратите внимание, здесь мы не использовали ключевое слово synchornized, потому что нам это не нужно, потому что весь код выполняется на EDT.

SwingUtilities.invokeLater - это всего лишь несколько методов для вызова Runnable в EDT в какой-то момент в будущем. Таким образом, нам также необходимо вызвать создание JFrame, JPanel и JRadioButton (или просто вызвать createAndShowGUI) в EDT, потому что это код, связанный с EDT (например, что, если событие был запущен при добавлении панели к фрейму? ...).

Я добавил несколько комментариев в код, чтобы помочь другим вещам, связанным с показанными примерами.

Дайте мне знать в комментируйте любые вопросы, которые могут возникнуть, и я обновлю свой ответ как можно скорее.

3 голосов
/ 02 августа 2020

Указанный код ничего не печатает. Он также не будет компилироваться, вам нужно исправить private static Thread;, чтобы сказать private static Thread thr;.

В любом случае, это может сработать или нет, в зависимости от того, что в коде отсутствует какая-либо синхронизация. Это означает, что изменения, внесенные в переменную в одном потоке, не обязательно будут видны в другом. Если для одной переменной изначально установлено значение false, а затем установлено значение true в одном потоке, второй поток по-прежнему может видеть кэшированное значение false.

Попробуйте сделать свой boolean переменные volatile и посмотрите, работает ли это, но настоящий ответ - это чтение синхронизации потоков , например, в Java Учебнике

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...