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

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

  • открытый класс Game { private GraphicalUserInterface userInterface ; public Game () { userInterface = new GraphicalUserInterface ( this ); } public void play () { int selection = 0 ; while ( выбор == 0 ) { selection = userInterface . getSelection (); } Система . из . println ( выбор ); } public static void main ( String [] args ) { Игровая игра = новая игра (); игра . play (); } } .java
  • GraphicalUserInterface.java

Графический пользовательский интерфейс отображает «Новый импорт java . Awt . BorderLayout ; import java . Awt . Event . ActionEvent ; import java . Awt . Event . ActionListener ; import javax . Swing . JButton ; import javax . Swing . JFrame ; import javax . свинг . JPanel , общественный класс GraphicalUserInterface расширяет JFrame реализует ActionListener { личное игры игры ; частный JButton newGameButton = новый JButton ( "New Game" ), частные JButton [] numberedButtons = новый JButton [ 3 ], частные JPanel southPanel = новый JPanel (); private int selection ; private boolean isItUsersTurn = false ; private boolean didUserMakeSelection = false ; public GraphicalUserInterface ( игровая игра ) { this . game = game ; newGameButton . addActionListener ( this ); for ( int i = 0 ; i < 3 ; i ++ ) { numberedButtons [ i ] = new JButton (( новый Integer ( i + 1 )). toString ()); numberedButtons [ i ]. addActionListener ( this ); southPanel . add ( numberedButtons [ i ]); } getContentPane (). добавить ( newGameButton , BorderLa ты . СЕВЕР ); getContentPane (). add ( southPanel , BorderLayout . SOUTH ); pack (); setDefaultCloseOperation ( фрейм . EXIT_ON_CLOSE ); setLocationRelativeTo ( null ); setVisible ( true ); } Общественного недействительными actionPerformed ( ActionEvent событие ) { JButton pressedButton = ( JButton ) событие . getSource (); if ( нажатоButton . getText () == "Новая игра" ) { игра . play (); } else if ( isItUsersTurn ) { selection = southPanel . getComponentZOrder ( нажатоButton ) + 1 ; didUserMakeSelection = true ; } } public int getSelection () { if (! isItUsersTurn ) { isItUsersTurn = true ; } if ( didUserMakeSelection ) { isItUsersTurn = false ; didUserMakeSelection = false ; выбор возврата ; } else { return 0 ; } } } ", а затем три других кнопки с номерами от 1 до 3.

Если пользователь нажимает на одну из пронумерованных кнопок, на экране выводится соответствующий номер на консоль. Однако, если пользователь нажимает кнопку « while ( selection == 0 ) { selection = userInterface . GetSelection (); } », программа замерзает.

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

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

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

Источник

Game.java :

play()

GraphicalUserInterface.java :

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

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

game.play()

в mainметоде Game.java .

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

game.play()

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

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

java,swing,user-interface,concurrency,

9

Ответов: 5


GraphicalUserInterface.java :

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

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

game.play()

в mainметоде Game.java .

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

game.play()

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

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

59
17

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

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

Когда программа сначала запускается, play()выполняется выполнение по вызову select == 0 , который выполняет поток main. Однако, когда нажата кнопка «Новая игра», она falseзапускается потоком отправки событий (вместо основного потока), который является потоком, ответственным за выполнение кода обработки событий и обновления пользовательского интерфейса. selection == 0Цикл (в false) только прекращается , если имеет didUserMakeSelectionзначение true. Единственный способ didUserMakeSelectionоценки true- если это whileстановится 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(); }

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

Поскольку проблема вызвана выполнением Executorsв потоке отправки событий, прямой ответ должен выполняться ExecutorServiceв другом потоке. Это может быть достигнуто путем замены

Future

с

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

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

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

(1) добавление операторов импорта для Game, и , в Game.javaprivate ExecutorService gameExecutor = Executors.newSingleThreadExecutor();Future

Game

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

private Future<?> gameTask;

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

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

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

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

(5) заменяя

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

с

public void play() {
    int selection = 0;

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

    System.out.println(selection);
}

и наконец,

(6) заменяя

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(" " + 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; } } }

main()

Рекомендации

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


3

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

  • setVisible() запускается в основной прикладной нити

  • Однажды New Gameвызывается в компоненте Swing, создается новый поток для обработки пользовательского интерфейса

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

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

Вам необходимо перепроектировать свое приложение:

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

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

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


1

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

0

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


0

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

Java, качели, пользовательский интерфейс, параллелизм,
Похожие вопросы