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

The*_*viv 9 java concurrency user-interface 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();
    }

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

使用while循环导致问题

while (selection == 0) {
    selection = userInterface.getSelection();
}
Run Code Online (Sandbox Code Playgroud)

Game.javaplay()方法中.

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

//while (selection == 0) {
    selection = userInterface.getSelection();
//}
Run Code Online (Sandbox Code Playgroud)

该程序不再冻结.

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

The*_*viv 17

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

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

程序首次启动时,game.play()主线程执行,主线程是执行的线程main.但是,当按下"新游戏"按钮时,game.play()事件调度线程(而不是主线程)执行,该线程负责执行事件处理代码并更新用户界面.该while环(在play())只有终止,如果selection == 0计算结果为false.只有这样,selection == 0计算结果为false是,如果didUserMakeSelectiontrue.唯一的办法didUserMakeSelection变得true是,如果用户按下数字按钮之一.但是,用户不能按任何编号按钮,也不能按"新游戏"按钮,也不能退出程序."新游戏"按钮甚至没有弹出,因为事件调度线程(否则会重新绘制屏幕)太忙于执行while循环(由于上述原因,这实际上是有效的).

(2)如何重写程序来解决问题?

由于问题是由game.play()事件调度线程中的执行引起的,因此直接答案是game.play()在另一个线程中执行.这可以通过更换来完成

if (pressedButton.getText() == "New Game") {
    game.play();
}
Run Code Online (Sandbox Code Playgroud)

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

但是,这会产生一个新的(虽然更容易忍受)问题:每次按下"新游戏"按钮时,都会创建一个新线程.由于程序非常简单,因此不是什么大问题; 一旦用户按下编号按钮,这样的线程就变为不活动(即游戏结束).但是,假设完成游戏需要更长的时间.假设,当游戏正在进行时,用户决定开始新的游戏.每次用户开始新游戏时(在完成一个游戏之前),活动线程的数量会增加.这是不合需要的,因为每个活动线程都消耗资源.

新问题可以通过以下方式解决:

(1)将导入语句Executors,ExecutorService以及Future,在Game.java

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
Run Code Online (Sandbox Code Playgroud)

(2)添加单线程执行器作为字段Game

private ExecutorService gameExecutor = Executors.newSingleThreadExecutor();
Run Code Online (Sandbox Code Playgroud)

(3)将Future表示提交给单线程执行程序的最后一个任务的a添加为一个字段Game

private Future<?> gameTask;
Run Code Online (Sandbox Code Playgroud)

(4)添加方法 Game

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

(5)更换

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

if (pressedButton.getText() == "New Game") {
    game.startNewGame();
}
Run Code Online (Sandbox Code Playgroud)

最后,

(6)更换

public void play() {
    int selection = 0;

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

    System.out.println(selection);
}
Run Code Online (Sandbox Code Playgroud)

public void play() {
    int selection = 0;

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

    System.out.println(selection);
}
Run Code Online (Sandbox Code Playgroud)

要确定if (Thread.currentThread().isInterrupted())检查的位置,请查看方法滞后的位置.在这种情况下,用户必须进行选择.

还有另一个问题.主线程仍然可以处于活动状态.要解决此问题,您可以替换

public static void main(String[] args) {
    Game game = new Game();
    game.play();
}
Run Code Online (Sandbox Code Playgroud)

public static void main(String[] args) {
    Game game = new Game();
    game.startNewGame();
}
Run Code Online (Sandbox Code Playgroud)

以下代码适用上述修改(除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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

参考

Java教程:课程:并发
Java教程:课程:Swing中
并发Java虚拟机规范,Java SE 7版
Java虚拟机规范,第二版
Eckel,Bruce.用Java思考,第4版."并发与摆动:长期运行的任务",p.988.
如何在同一个线程上取消正在运行的任务并将其替换为新任务?


bob*_*bah 0

事件回调在GUI事件处理线程中执行(Swig是单线程的)。在回调中您无法获取任何其他事件,因此您的 while 循环永远不会终止。这并不是要考虑这样一个事实:在 java 中,从多个线程访问的变量应该是易失性的或原子的,或者使用同步原语进行保护。