为什么这个简单的Java Swing程序会冻结?

时间:2012-02-23 18:48:14

标签: java swing user-interface concurrency

下面是一个简单的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();
}
Game.java play()方法中的

如果第12和14行被注释掉,

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

程序不再冻结。

我认为问题与并发性有关。但是,我想准确理解while循环导致程序冻结的原因。

5 个答案:

答案 0 :(得分:17)

谢谢各位程序员。我发现答案非常有用。

  

(1)为什么程序会冻结?

程序首次启动时,{em>主线程执行game.play(),这是执行main的线程。但是,当按下“新游戏”按钮时,{em>事件调度线程(而不是主线程)执行game.play(),这是负责执行事件处理代码的线程并更新用户界面。 while循环(play())仅在selection == 0评估为false时终止。 selection == 0评估为false的唯一方法是didUserMakeSelection变为truedidUserMakeSelection成为true的唯一方法是用户按下其中一个编号按钮。但是,用户不能按任何编号按钮,也不能按“新游戏”按钮,也不能退出程序。 “新游戏”按钮甚至没有弹出,因为事件调度线程(否则会重新绘制屏幕)太忙于执行while循环(由于上述原因,它实际上是无效的)。 / p>

  

(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)在 Game.java

中添加ExecutorsExecutorServiceFuture的导入语句
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;
        }
    }
}

参考

The Java Tutorials: Lesson: Concurrency
The Java Tutorials: Lesson: Concurrency in Swing
The Java Virtual Machine Specification, Java SE 7 Edition
The Java Virtual Machine Specification, Second Edition
Eckel, Bruce. Thinking in Java, 4th Edition. "Concurrency & Swing: Long-running tasks", p. 988.
How do I cancel a running task and replace it with a new one, on the same thread?

答案 1 :(得分:3)

奇怪的是,这个问题与并发性没有关系,尽管你的程序也充满了这方面的问题:

  • main()在主申请主题

  • 中启动
  • 在Swing组件中调用setVisible()后,会创建一个新线程来处理用户界面

  • 用户按下New Game按钮后, UI线程主线程)通过ActionEvent调用监听Game.play()方法进入无限循环:UI线程通过getSelection()方法不断轮询自己的字段,而没有机会继续处理UI和任何新输入来自用户的事件。

    本质上,您正在从应该更改它们的同一个线程中轮询一组字段 - 一个保证无限循环,使Swing事件循环不会获取新事件或更新显示。

您需要重新设计应用程序:

  • 在我看来,getSelection()的返回值只能在一些用户操作后更改。在这种情况下,实际上不需要轮询它 - 在UI线程中检查一次就足够了。

  • 对于非常简单的操作,例如仅在用户执行操作后更新显示的简单游戏,可能足以在事件侦听器中执行所有计算,而不会出现任何响应问题。

    < / LI>
  • 对于更复杂的情况,例如如果您需要在没有用户干预的情况下更新UI,例如在下载文件时填写的进度条,则需要在单独的线程中执行实际工作,并使用synchronization来协调UI更新。 / p>

答案 2 :(得分: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);
    }
}

答案 3 :(得分:0)

事件回调在GUI事件处理线程中执行(Swig是单线程的)。在回调中你不能得到任何其他事件,所以你的while循环永远不会被终止。这不是要考虑这样一个事实:在java中,从多个线程访问的变量应该是volatile或atomic,或者使用同步原语进行保护。

答案 4 :(得分:0)

我观察到的是最初的didUserMakeSelection为false。因此,当从while循环调用时它总是返回0并且控制将在while循环中保持循环。