Почему зависает эта простая программа Java Swing?

Ниже представлена ​​простая программа Java Swing, состоящая из двух файлов:

  • Game.java
  • GraphicalUserInterface.java

В графическом пользовательском интерфейсе отображается кнопка «Новая игра», за которой следуют три другие кнопки, пронумерованные от 1 до 3.

Если пользователь нажимает одну из пронумерованных кнопок, игра выводит соответствующий номер на консоль. Однако, если пользователь нажимает кнопку «Новая игра», программа зависает.

(1) Почему программа зависает?

(2) Как можно переписать программу, чтобы устранить проблему?

(3) Как вообще можно лучше написать программу?

Источник

Game.java:

public class Game {

    private GraphicalUserInterface userInterface;

    public Game() {
        userInterface = new GraphicalUserInterface(this);
    }

    public void play() {
        int selection = 0;

        while (selection == 0) {
            selection = userInterface.getSelection();
        }

        System.out.println(selection);
    }

    public static void main(String[] args) {
        Game game = new Game();
        game.play();
    }

}

GraphicalUserInterface.java:

import java.awt.BorderLayout;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class GraphicalUserInterface extends JFrame implements ActionListener {

    private Game game;
    private JButton newGameButton = new JButton("New Game");
    private JButton[] numberedButtons = new JButton[3];
    private JPanel southPanel = new JPanel();
    private int selection;
    private boolean isItUsersTurn = false;
    private boolean didUserMakeSelection = false;

    public GraphicalUserInterface(Game game) {
        this.game = game;

        newGameButton.addActionListener(this);

        for (int i = 0; i < 3; i++) {
            numberedButtons[i] = new JButton((new Integer(i+1)).toString());
            numberedButtons[i].addActionListener(this);
            southPanel.add(numberedButtons[i]);
        }

        getContentPane().add(newGameButton, BorderLayout.NORTH);
        getContentPane().add(southPanel, BorderLayout.SOUTH);

        pack();
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public void actionPerformed(ActionEvent event) {
        JButton pressedButton = (JButton) event.getSource();

        if (pressedButton.getText() == "New Game") {
            game.play();
        }
        else if (isItUsersTurn) {
            selection = southPanel.getComponentZOrder(pressedButton) + 1;
            didUserMakeSelection = true;
        }
    }

    public int getSelection() {
        if (!isItUsersTurn) {
            isItUsersTurn = true;
        }

        if (didUserMakeSelection) {
            isItUsersTurn = false;
            didUserMakeSelection = false;
            return selection;
        }
        else {
            return 0;
        }
    }

}

Проблема возникает из-за использования цикла while

while (selection == 0) {
    selection = userInterface.getSelection();
}

в play() методе Game.java.

Если строки 12 и 14 закомментированы,

//while (selection == 0) {
    selection = userInterface.getSelection();
//}

программа больше не зависает.

Думаю, проблема связана с параллелизмом. Однако я хотел бы получить точное представление о том, почему цикл while приводит к зависанию программы.


person The Aviv    schedule 23.02.2012    source источник
comment
В чем смысл цикла while в этой программе? Он запаздывает, потому что это бесконечный цикл.   -  person Mob    schedule 23.02.2012
comment
Это называется бесконечным циклом.   -  person Romain Hippeau    schedule 23.02.2012
comment
Целью цикла while является ожидание, пока пользователь сделает выбор. Метод getSelection() возвращает число, когда настала очередь пользователя.   -  person The Aviv    schedule 02.04.2012


Ответы (5)


Спасибо товарищам-программистам. Я нашел ответы очень полезными.

(1) Почему программа зависает?

При первом запуске программы game.play() выполняется основным потоком, который является потоком, выполняющим main. Однако при нажатии кнопки «Новая игра» game.play() запускается потоком отправки событий (вместо основного потока), который является потоком, ответственным за выполнение кода обработки событий и обновление пользователя. интерфейс. Цикл whileplay()) завершается, только если selection == 0 оценивается как false. Единственный способ, которым selection == 0 оценивается как false, - это если didUserMakeSelection становится true. Единственный способ, которым didUserMakeSelection становится true, - это если пользователь нажимает одну из пронумерованных кнопок. Однако пользователь не может нажать ни нумерованную кнопку, ни кнопку «Новая игра», ни выйти из программы. Кнопка «Новая игра» даже не выскакивает обратно, потому что поток отправки событий (который в противном случае перерисовал бы экран) слишком занят выполнением цикла while (который фактически является inifinte по указанным выше причинам).

(2) Как можно переписать программу, чтобы устранить проблему?

Поскольку проблема вызвана выполнением game.play() в потоке отправки событий, прямой ответ - выполнить game.play() в другом потоке. Этого можно добиться, заменив

if (pressedButton.getText() == "New Game") {
    game.play();
}

с участием

if (pressedButton.getText() == "New Game") {
    Thread thread = new Thread() {
        public void run() {
            game.play();
        }
    };
    thread.start();
}

Однако это приводит к новой (хотя и более терпимой) проблеме: каждый раз, когда нажимается кнопка «Новая игра», создается новый поток. Поскольку программа очень проста, это не имеет большого значения; такой поток становится неактивным (т.е. игра завершается), как только пользователь нажимает пронумерованную кнопку. Однако предположим, что для завершения игры потребовалось больше времени. Предположим, в процессе игры пользователь решает начать новую. Каждый раз, когда пользователь запускает новую игру (до ее завершения), количество активных потоков увеличивается. Это нежелательно, потому что каждый активный поток потребляет ресурсы.

Новую проблему можно исправить:

(1) добавление операторов импорта для Executors, ExecutorService и Future в Game.java

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

(2) добавление однопоточного исполнителя в поле под Game

private ExecutorService gameExecutor = Executors.newSingleThreadExecutor();

(3) добавление Future, представляющего последнюю задачу, переданную однопотоковому исполнителю, в виде поля под Game

private Future<?> gameTask;

(4) добавление метода под Game

public void startNewGame() {
    if (gameTask != null) gameTask.cancel(true);
    gameTask = gameExecutor.submit(new Runnable() {
        public void run() {
            play();
        }
    });
}

(5) замена

if (pressedButton.getText() == "New Game") {
    Thread thread = new Thread() {
        public void run() {
            game.play();
        }
    };
    thread.start();
}

с участием

if (pressedButton.getText() == "New Game") {
    game.startNewGame();
}

и наконец,

(6) замена

public void play() {
    int selection = 0;

    while (selection == 0) {
        selection = userInterface.getSelection();
    }

    System.out.println(selection);
}

с участием

public void play() {
    int selection = 0;

    while (selection == 0) {
        selection = userInterface.getSelection();
        if (Thread.currentThread().isInterrupted()) {
            return;
        }
    }

    System.out.println(selection);
}

Чтобы определить, где поставить if (Thread.currentThread().isInterrupted()) проверку, посмотрите, где отстает метод. В этом случае пользователь должен сделать выбор.

Есть еще одна проблема. Основной поток все еще может быть активен. Чтобы исправить это, вы можете заменить

public static void main(String[] args) {
    Game game = new Game();
    game.play();
}

с участием

public static void main(String[] args) {
    Game game = new Game();
    game.startNewGame();
}

В приведенном ниже коде применяются указанные выше модификации (в дополнение к методу checkThreads()):

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class Game {
    private GraphicalUserInterface userInterface;
    private ExecutorService gameExecutor = Executors.newSingleThreadExecutor();
    private Future<?> gameTask;

    public Game() {
        userInterface = new GraphicalUserInterface(this);
    }

    public static void main(String[] args) {
        checkThreads();
        Game game = new Game();
        checkThreads();
        game.startNewGame();
        checkThreads();
    }

    public static void checkThreads() {
        ThreadGroup mainThreadGroup = Thread.currentThread().getThreadGroup();
        ThreadGroup systemThreadGroup = mainThreadGroup.getParent();

        System.out.println("\n" + Thread.currentThread());
        systemThreadGroup.list();
    }

    public void play() {
        int selection = 0;

        while (selection == 0) {
            selection = userInterface.getSelection();
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
        }

        System.out.println(selection);
    }

    public void startNewGame() {
        if (gameTask != null) gameTask.cancel(true);
        gameTask = gameExecutor.submit(new Runnable() {
            public void run() {
                play();
            }
        });
    }
}

class GraphicalUserInterface extends JFrame implements ActionListener {
    private Game game;
    private JButton newGameButton = new JButton("New Game");
    private JButton[] numberedButtons = new JButton[3];
    private JPanel southPanel = new JPanel();
    private int selection;
    private boolean isItUsersTurn = false;
    private boolean didUserMakeSelection = false;

    public GraphicalUserInterface(Game game) {
        this.game = game;

        newGameButton.addActionListener(this);

        for (int i = 0; i < 3; i++) {
            numberedButtons[i] = new JButton((new Integer(i+1)).toString());
            numberedButtons[i].addActionListener(this);
            southPanel.add(numberedButtons[i]);
        }

        getContentPane().add(newGameButton, BorderLayout.NORTH);
        getContentPane().add(southPanel, BorderLayout.SOUTH);

        pack();
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public void actionPerformed(ActionEvent event) {
        JButton pressedButton = (JButton) event.getSource();

        if (pressedButton.getText() == "New Game") {
            game.startNewGame();
            Game.checkThreads();
        }
        else if (isItUsersTurn) {
            selection = southPanel.getComponentZOrder(pressedButton) + 1;
            didUserMakeSelection = true;
        }
    }

    public int getSelection() {
        if (!isItUsersTurn) {
            isItUsersTurn = true;
        }

        if (didUserMakeSelection) {
            isItUsersTurn = false;
            didUserMakeSelection = false;
            return selection;
        }
        else {
            return 0;
        }
    }
}

использованная литература

Учебники по Java: Урок: параллелизм
Учебники по Java: Урок: Параллелизм в Swing
Спецификация виртуальной машины Java, Java SE 7 Edition
Спецификация виртуальной машины Java, второе издание
Экель, Брюс. Мышление на Java, 4-е издание. «Параллелизм и Swing: длительные задачи», стр. 988.
Как мне отменить запущенную задачу и заменить ее новой в том же потоке?

person The Aviv    schedule 02.04.2012
comment
Действительно хорошее решение! Использование Executor и Future позволяет избежать зависания интерфейса. +1 - person logo_writer; 11.09.2016

Как ни странно, эта проблема не связана с параллелизмом, хотя ваша программа также чревата проблемами в этом отношении:

  • main() запускается в основном потоке приложения

  • Как только setVisible() вызывается в компоненте Swing, создается новый поток для обработки пользовательского интерфейса.

  • Как только пользователь нажимает кнопку New Game, поток пользовательского интерфейса (не основной поток) вызывает через ActionEvent слушателя метод Game.play(), который переходит в бесконечный цикл: пользовательский интерфейс поток постоянно опрашивает свои собственные поля с помощью метода getSelection(), не имея возможности продолжить работу с пользовательским интерфейсом и любыми новыми событиями ввода от пользователя.

    По сути, вы опрашиваете набор полей из того же потока, который должен их изменить - гарантированный бесконечный цикл, который не позволяет циклу событий Swing получать новые события или обновлять отображение.

Вам необходимо изменить дизайн вашего приложения:

  • Мне кажется, что возвращаемое значение getSelection() может измениться только после некоторого действия пользователя. В этом случае действительно нет необходимости опрашивать его - проверки один раз в потоке пользовательского интерфейса должно быть достаточно.

  • Для очень простых операций, таких как простая игра, которая обновляет отображение только после того, как пользователь что-то делает, может быть достаточно выполнить все вычисления в прослушивателях событий без каких-либо проблем с быстродействием.

  • Для более сложных случаев, например если вам нужно, чтобы пользовательский интерфейс обновлялся без вмешательства пользователя, например, индикатор выполнения, который заполняется по мере загрузки файла, вам необходимо выполнять свою фактическую работу в отдельных потоках и использовать synchronization для координации обновлений пользовательского интерфейса.

person thkala    schedule 23.02.2012

(3) Как вообще можно лучше написать программу?

Я немного отредактировал ваш код и предположил, что вы можете превратить его в игру в угадывание. Я объясню некоторые рефакторинги:

Во-первых, нет необходимости в игровом цикле, пользовательский интерфейс предоставляет это по умолчанию. Затем для приложений Swing вы действительно должны поместить компоненты в очередь событий, как я сделал с invokeLater. Слушатели действий действительно должны быть анонимными внутренними классами, если нет причин для их повторного использования, поскольку это сохраняет логику инкапсулированной.

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

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class Game {

    private int prize;
    private Random r = new Random();

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new UserInterface(new Game()));
    }

    public void play() {
        System.out.println("Please Select a number...");
        prize = r.nextInt(3) + 1;
    }

    public void buttonPressed(int button) {
        String message = (button == prize) ? "you win!" : "sorry, try again";
        System.out.println(message);

    }
}

class UserInterface implements Runnable {

    private final Game game;

    public UserInterface(Game game) {
        this.game = game;
    }

    @Override
    public void run() {
        JFrame frame = new JFrame();
        final JButton newGameButton = new JButton("New Game");
        newGameButton.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent arg0) {
                game.play();
            }
        });

        JPanel southPanel = new JPanel();
        for (int i = 1; i <= 3; i++) {
            final JButton button = new JButton("" + i);
            button.addActionListener(new ActionListener() {

                public void actionPerformed(ActionEvent event) {
                    game.buttonPressed(Integer.parseInt(button.getText()));
                }
            });
            southPanel.add(button);
        }

        frame.add(newGameButton, BorderLayout.NORTH);
        frame.add(southPanel, BorderLayout.SOUTH);

        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}
person Robert    schedule 23.02.2012

Обратный вызов события выполняется в потоке обработки событий графического интерфейса пользователя (Swig является однопоточным). Вы не можете получить какое-либо другое событие во время обратного вызова, поэтому цикл while никогда не завершается. Это не должно учитывать тот факт, что в java переменная, доступ к которой осуществляется из нескольких потоков, должна быть либо изменчивой, либо атомарной, либо защищенной примитивами синхронизации.

person bobah    schedule 23.02.2012

Я заметил, что изначально didUserMakeSelection ложно. Таким образом, он всегда возвращает 0 при вызове из цикла while, и управление будет продолжать цикл while.

person JProgrammer    schedule 23.02.2012