为什么这个简单的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.javaplay()方法中。

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

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

该程序不再冻结。

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

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

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

程序首次启动时, game.play()主线程执行, 主线程是执行main的线程。 但是,当按下“新游戏”按钮时, game.play()事件调度线程 (而不是主线程)执行,该线程负责执行事件处理代码并更新用户界面。 while循环(在play() )仅在selection == 0计算结果为false终止。 如果didUserMakeSelection变为trueselection == 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中ExecutorsExecutorServiceFuture添加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循环。