Почему эта простая программа Java Swing зависает? - PullRequest
9 голосов
/ 23 февраля 2012

Ниже приведена простая программа на 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 loop

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

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

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

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

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

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

Ответы [ 5 ]

17 голосов
/ 02 апреля 2012

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

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

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

(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 VirtualСпецификация машины, Java SE 7 Edition
Спецификация виртуальной машины Java, второе издание
Eckel, Брюс. Мышление на Java, 4-е издание .«Параллельность и свинг: долгосрочные задачи», с.988.
Как отменить запущенное задание и заменить его новым в том же потоке?

3 голосов
/ 23 февраля 2012

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

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

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

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

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

Вам необходимо изменить дизайн вашего application:

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

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

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

1 голос
/ 24 февраля 2012

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

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

Во-первых, игровой цикл не нужен, пользовательский интерфейс обеспечивает это по умолчанию. Далее, для свинг-приложений вы должны действительно поместить компоненты в очередь событий, как я сделал с 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 голосов
/ 23 февраля 2012

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

0 голосов
/ 23 февраля 2012

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

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