下面是一个简单的Java Swing程序,它由两个文件组成:
图形用户界面显示“新游戏”按钮,然后显示编号为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
循环导致程序冻结的原因。
答案 0 :(得分:17)
谢谢各位程序员。我发现答案非常有用。
(1)为什么程序会冻结?
程序首次启动时,{em>主线程执行game.play()
,这是执行main
的线程。但是,当按下“新游戏”按钮时,{em>事件调度线程(而不是主线程)执行game.play()
,这是负责执行事件处理代码的线程并更新用户界面。 while
循环(play()
)仅在selection == 0
评估为false
时终止。 selection == 0
评估为false
的唯一方法是didUserMakeSelection
变为true
。 didUserMakeSelection
成为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
中添加Executors
,ExecutorService
和Future
的导入语句
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循环中保持循环。