为什么这个简单的Java 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(); } }
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
循环导致程序冻结的原因。
谢谢各位程序员。 我发现答案非常有用。
(1)为什么程序会冻结?
程序首次启动时, game.play()
由主线程执行, 主线程是执行main
的线程。 但是,当按下“新游戏”按钮时, game.play()
由事件调度线程 (而不是主线程)执行,该线程负责执行事件处理代码并更新用户界面。 while
循环(在play()
)仅在selection == 0
计算结果为false
终止。 如果didUserMakeSelection
变为true
则selection == 0
求值为false
的唯一方法。 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)在Game.java中为Executors
, ExecutorService
和Future
添加import语句
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虚拟机规范,Java SE 7 Edition
Java虚拟机规范,第二版
埃克尔,布鲁斯。 思考Java,第4版 。 “并发与摆动:长期运行的任务”,p。 988。
如何在同一个线程上取消正在运行的任务并将其替换为新任务?
奇怪的是,这个问题与并发性没有关系,尽管你的程序在这方面也充满了问题:
-
main()
在主应用程序线程中启动 -
在Swing组件中调用
setVisible()
,将创建一个新线程来处理用户界面 -
一旦用户按下
New Game
按钮, UI线程 ( 不是主线程)通过ActionEvent
侦听器调用Game.play()
方法,该方法进入无限循环:UI线程通过不断循环查询自己的字段getSelection()
方法,没有机会继续处理UI和用户的任何新输入事件。本质上,您正在从应该更改它们的同一线程中轮询一组字段 – 一个保证无限循环,使Swing事件循环不会获取新事件或更新显示。
您需要重新设计您的应用程序:
-
在我看来,
getSelection()
的返回值只能在一些用户操作后更改。 在这种情况下,实际上没有必要轮询它 – 在UI线程中检查一次就足够了。 -
对于非常简单的操作,例如仅在用户执行某些操作后更新显示的简单游戏,可能足以在事件侦听器中执行所有计算,而没有任何响应问题。
-
对于更复杂的情况,例如,如果您需要在没有用户干预的情况下更新UI,例如在下载文件时填充的进度条,则需要在单独的线程中执行实际工作并使用同步来协调UI更新。
(3)如何更好地编写程序?
我稍微重构了你的代码,并猜测你可能想把它变成一个猜谜游戏。 我将解释一些重构:
首先,不需要游戏循环,用户界面默认提供此function。 接下来,对于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); } }
事件回调在GUI事件处理线程中执行(Swig是单线程的)。 在回调中你不能得到任何其他事件,所以你的while循环永远不会被终止。 这不是要考虑这样一个事实:在java中,从多个线程访问的变量应该是易失性的或primefaces的,或者用同步原语保护。
我观察到的是最初的didUserMakeSelection是假的。 所以当从while循环调用时它总是返回0并且控制将保持循环进入while循环。