Pourquoi ce simple programme Java Swing gèle-t-il?

Vous trouverez ci-dessous un programme Java Swing simple composé de deux fichiers:

  • Game.java
  • GraphicalUserInterface.java

L’interface utilisateur graphique affiche un bouton “Nouvelle partie”, suivi de trois autres boutons numérotés de 1 à 3.

Si l’utilisateur clique sur l’un des boutons numérotés, le jeu imprime le numéro correspondant sur la console. Toutefois, si l’utilisateur clique sur le bouton “Nouveau jeu”, le programme se fige.

(1) Pourquoi le programme se fige-t-il?

(2) Comment le programme peut-il être réécrit pour résoudre le problème?

(3) Comment le programme peut-il être mieux écrit en général?

La source

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(Ssortingng[] 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; } } } 

Le problème provient de l’utilisation de la boucle while

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

dans la méthode play() de Game.java .

Si les lignes 12 et 14 sont commentées,

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

le programme ne gèle plus.

Je pense que le problème est lié à la concurrence. Cependant, je voudrais bien comprendre pourquoi la boucle while gèle le programme.

Merci collègues programmeurs. J’ai trouvé les réponses très utiles.

(1) Pourquoi le programme se fige-t-il?

Lorsque le programme démarre pour la première fois, game.play() est exécuté par le thread principal , qui est le thread qui exécute main . Cependant, lorsque vous game.play() bouton “Nouveau jeu”, game.play() est exécuté par le thread d’envoi d’événements (au lieu du thread principal), qui est responsable de l’exécution du code de gestion des événements et de la mise à jour de l’interface utilisateur. La boucle while (dans play() ) ne se termine que si selection == 0 false . La seule façon dont la selection == 0 est didUserMakeSelection sur false est si didUserMakeSelection devient true . didUserMakeSelection devient true uniquement si l’utilisateur appuie sur l’un des boutons numérotés. Cependant, l’utilisateur ne peut appuyer sur aucun bouton numéroté, ni sur le bouton “Nouveau jeu”, ni quitter le programme. Le bouton “Nouvelle partie” ne réapparaît même pas, car le fil de répartition des événements (qui repeindrait sinon l’écran) est trop occupé à exécuter la boucle while (qui est inifinte pour les raisons ci-dessus).

(2) Comment le programme peut-il être réécrit pour résoudre le problème?

Comme le problème est dû à l’exécution de game.play() dans le thread d’envoi d’événements, la réponse directe consiste à exécuter game.play() dans un autre thread. Ceci peut être accompli en remplaçant

 if (pressedButton.getText() == "New Game") { game.play(); } 

avec

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

Cependant, il en résulte un nouveau problème (bien que plus tolérable): chaque fois que le bouton “Nouveau jeu” est pressé, un nouveau fil est créé. Comme le programme est très simple, ce n’est pas grave. un tel fil devient inactif (c.-à-d. qu’un jeu se termine) dès que l’utilisateur appuie sur un bouton numéroté. Cependant, supposons qu’il ait fallu plus de temps pour terminer une partie. Supposons que, pendant le déroulement d’un jeu, l’utilisateur décide de commencer un nouveau. Chaque fois que l’utilisateur commence une nouvelle partie (avant d’en terminer une), le nombre de threads actifs augmente. Cela n’est pas souhaitable, car chaque thread actif consum des ressources.

Le nouveau problème peut être résolu par:

(1) append des instructions d’importation pour Executors , ExecutorService et Future , dans Game.java

 import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; 

(2) append un exécuteur à un seul thread en tant que champ sous Game

 private ExecutorService gameExecutor = Executors.newSingleThreadExecutor(); 

(3) append un Future , représentant la dernière tâche soumise à l’ exécuteur à thread unique , en tant que champ sous Game

 private Future gameTask; 

(4) append une méthode sous Game

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

(5) remplacer

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

avec

 if (pressedButton.getText() == "New Game") { game.startNewGame(); } 

et enfin,

(6) remplacement

 public void play() { int selection = 0; while (selection == 0) { selection = userInterface.getSelection(); } System.out.println(selection); } 

avec

 public void play() { int selection = 0; while (selection == 0) { selection = userInterface.getSelection(); if (Thread.currentThread().isInterrupted()) { return; } } System.out.println(selection); } 

Pour déterminer où placer la if (Thread.currentThread().isInterrupted()) , regardez où la méthode est en retard. Dans ce cas, c’est à l’utilisateur de faire une sélection.

Il y a un autre problème. Le fil principal pourrait toujours être actif. Pour résoudre ce problème, vous pouvez remplacer

 public static void main(Ssortingng[] args) { Game game = new Game(); game.play(); } 

avec

 public static void main(Ssortingng[] args) { Game game = new Game(); game.startNewGame(); } 

Le code ci-dessous applique les modifications ci-dessus (en plus d’une méthode 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(Ssortingng[] 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; } } } 

Les références

Les tutoriels Java: Leçon: Concurrence
Les tutoriels Java: Leçon: Concurrence dans Swing
Spécification de la machine virtuelle Java, Java SE 7 Edition
La spécification de la machine virtuelle Java, deuxième édition
Eckel, Bruce. Penser en Java, 4ème édition . "Concurrence & Swing: tâches de longue durée", p. 988.
Comment puis-je annuler une tâche en cours et la remplacer par une nouvelle sur le même thread?

Curieusement, ce problème n’a pas à voir avec la simultanéité, bien que votre programme présente également de nombreux problèmes à cet égard:

  • main() est lancé dans le fil principal de l’application

  • Une fois que setVisible() est appelé dans un composant Swing, un nouveau thread est créé pour gérer l’interface utilisateur.

  • Lorsqu’un utilisateur appuie sur le bouton New Game , le thread d’interface utilisateur (et non le thread principal) appelle, via l’écouteur Game.play() , la méthode Game.play() , qui passe dans une boucle infinie: le thread d’interface utilisateur interroge en permanence ses propres champs via l’interface utilisateur. getSelection() méthode getSelection() , sans jamais avoir la possibilité de continuer à travailler sur l’interface utilisateur et les nouveaux événements d’entrée de l’utilisateur.

    Essentiellement, vous interrogez un ensemble de champs du même thread censé les modifier – une boucle infinie garantie qui empêche la boucle d’événements Swing d’obtenir de nouveaux événements ou de mettre à jour l’affichage.

Vous devez repenser votre application:

  • Il me semble que la valeur de retour de getSelection() ne peut changer qu’après certaines actions de l’utilisateur. Dans ce cas, il n’est vraiment pas nécessaire de l’interroger – une seule vérification dans le thread d’interface utilisateur devrait suffire.

  • Pour des opérations très simples, telles qu’un jeu simple qui met à jour l’affichage uniquement après que l’utilisateur a fait quelque chose, il peut être suffisant d’effectuer tous les calculs dans les écouteurs d’événements, sans aucun problème de réactivité.

  • Pour des cas plus complexes, par exemple si vous souhaitez que l’interface utilisateur se mette à jour sans intervention de l’utilisateur, telle qu’une barre de progression se remplissant au fur et à mesure qu’un fichier est téléchargé, vous devez effectuer votre travail réel dans des threads séparés et utiliser la synchronisation pour coordonner les mises à jour de l’interface utilisateur. .

(3) Comment le programme peut-il être mieux écrit en général?

J’ai un peu remodelé votre code et je suppose que vous aimeriez peut-être en faire un jeu de devinettes. Je vais expliquer certaines des refactorings:

Premièrement, une boucle de jeu n’est pas nécessaire, l’interface utilisateur le fournit par défaut. Ensuite, pour les applications swing, vous devez vraiment placer des composants dans la queue des événements, comme je l’ai fait avec invokeLater. Les écouteurs d’action doivent vraiment être des classes internes anonymes, à moins qu’il n’y ait une raison de les réutiliser, car la logique rest encapsulée.

J’espère que cela vous servira d’exemple pour finir d’écrire le jeu que vous vouliez.

 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(Ssortingng[] 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) { Ssortingng 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); } } 

Le rappel d’événement est exécuté dans le thread de traitement des événements de l’interface graphique (Swig est à un seul thread). Vous ne pouvez obtenir aucun autre événement pendant le rappel afin que votre boucle while ne soit jamais terminée. Cela ne veut pas dire qu’en Java, une variable accessible à partir de plusieurs threads doit être volatile ou atomique ou protégée par des primitives de synchronisation.

Ce que j’ai observé, c’est qu’au départ didUserMakeSelection est faux. Donc, il retourne toujours 0 lorsqu’il est appelé depuis une boucle while et le contrôle restra en boucle.