La JVM est autorisée à supposer que les autres threads ne modifient pas la pizzaArrived
variable pendant la boucle. En d'autres termes, il peut hisser le pizzaArrived == false
test en dehors de la boucle, en optimisant ceci:
while (pizzaArrived == false) {}
dans ceci:
if (pizzaArrived == false) while (true) {}
qui est une boucle infinie.
Pour vous assurer que les modifications apportées par un thread sont visibles par les autres threads, vous devez toujours ajouter une synchronisation entre les threads. Le moyen le plus simple de procéder est de créer la variable partagée volatile
:
volatile boolean pizzaArrived = false;
Créer une variable volatile
garantit que les différents threads verront les effets des changements de chacun. Cela empêche la JVM de mettre en cache la valeur pizzaArrived
ou de hisser le test en dehors de la boucle. Au lieu de cela, il doit lire la valeur de la variable réelle à chaque fois.
(Plus formellement, volatile
crée une relation qui arrive avant entre les accès à la variable. Cela signifie que tout autre travail effectué par un thread avant de livrer la pizza est également visible par le thread recevant la pizza, même si ces autres modifications ne concernent pas des volatile
variables.)
Les méthodes synchronisées sont principalement utilisées pour implémenter l'exclusion mutuelle (empêchant que deux choses se produisent en même temps), mais elles ont également les mêmes effets secondaires volatile
. Les utiliser lors de la lecture et de l'écriture d'une variable est un autre moyen de rendre les modifications visibles par d'autres threads:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
while (getPizzaArrived() == false) {}
System.out.println("That was delicious!");
}
synchronized boolean getPizzaArrived() {
return pizzaArrived;
}
synchronized void deliverPizza() {
pizzaArrived = true;
}
}
L'effet d'une instruction d'impression
System.out
est un PrintStream
objet. Les méthodes de PrintStream
sont synchronisées comme ceci:
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
La synchronisation empêche la pizzaArrived
mise en cache pendant la boucle. Strictement parlant, les deux threads doivent se synchroniser sur le même objet pour garantir que les modifications apportées à la variable sont visibles. (Par exemple, un appel println
après la configuration pizzaArrived
et un nouvel appel avant la lecture pizzaArrived
serait correct.) Si un seul thread se synchronise sur un objet particulier, la JVM est autorisée à l'ignorer. En pratique, la JVM n'est pas assez intelligente pour prouver que les autres threads n'appelleront pas println
après la configuration pizzaArrived
, donc elle suppose qu'ils pourraient le faire. Par conséquent, il ne peut pas mettre en cache la variable pendant la boucle si vous appelez System.out.println
. C'est pourquoi des boucles comme celle-ci fonctionnent lorsqu'elles ont une instruction d'impression, bien que ce ne soit pas une solution correcte.
L'utilisation System.out
n'est pas le seul moyen de provoquer cet effet, mais c'est celui que les gens découvrent le plus souvent lorsqu'ils essaient de déboguer pourquoi leur boucle ne fonctionne pas!
Le plus gros problème
while (pizzaArrived == false) {}
est une boucle d'attente occupée. C'est mauvais! Pendant qu'il attend, il monopolise le processeur, ce qui ralentit les autres applications et augmente la consommation d'énergie, la température et la vitesse du ventilateur du système. Idéalement, nous aimerions que le thread de la boucle se mette en veille pendant qu'il attend, afin de ne pas monopoliser le processeur.
Voici quelques moyens de le faire:
Utilisation de l'attente / notification
Une solution de bas niveau consiste à utiliser les méthodes d'attente / notification deObject
:
class MyHouse {
boolean pizzaArrived = false;
void eatPizza() {
synchronized (this) {
while (!pizzaArrived) {
try {
this.wait();
} catch (InterruptedException e) {}
}
}
System.out.println("That was delicious!");
}
void deliverPizza() {
synchronized (this) {
pizzaArrived = true;
this.notifyAll();
}
}
}
Dans cette version du code, le thread de boucle appelle wait()
, ce qui met le thread en veille. Il n'utilisera aucun cycle de processeur pendant le sommeil. Une fois que le deuxième thread a défini la variable, il appelle notifyAll()
à réveiller tous les threads qui attendaient cet objet. C'est comme si le livreur de pizza sonnait à la porte, pour que vous puissiez vous asseoir et vous reposer en attendant, au lieu de vous tenir maladroitement à la porte.
Lorsque vous appelez wait / notify sur un objet, vous devez maintenir le verrou de synchronisation de cet objet, ce que fait le code ci-dessus. Vous pouvez utiliser n'importe quel objet que vous aimez tant que les deux threads utilisent le même objet: ici j'ai utilisé this
(l'instance de MyHouse
). Habituellement, deux threads ne pourraient pas entrer simultanément des blocs synchronisés du même objet (ce qui fait partie du but de la synchronisation) mais cela fonctionne ici car un thread libère temporairement le verrou de synchronisation lorsqu'il est à l'intérieur de la wait()
méthode.
BlockingQueue
A BlockingQueue
est utilisé pour implémenter des files d'attente producteur-consommateur. Les «consommateurs» prennent les articles du début de la file d'attente et les «producteurs» les poussent à l'arrière. Un exemple:
class MyHouse {
final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();
void eatFood() throws InterruptedException {
// take next item from the queue (sleeps while waiting)
Object food = queue.take();
// and do something with it
System.out.println("Eating: " + food);
}
void deliverPizza() throws InterruptedException {
// in producer threads, we push items on to the queue.
// if there is space in the queue we can return immediately;
// the consumer thread(s) will get to it later
queue.put("A delicious pizza");
}
}
Remarque: Les méthodes put
et take
de BlockingQueue
peuvent lancer des InterruptedException
s, qui sont des exceptions vérifiées qui doivent être gérées. Dans le code ci-dessus, pour plus de simplicité, les exceptions sont renvoyées. Vous préférerez peut-être intercepter les exceptions dans les méthodes et réessayer l'appel put ou take pour être sûr qu'il réussit. En dehors de ce point de laideur, il BlockingQueue
est très facile à utiliser.
Aucune autre synchronisation n'est nécessaire ici car a BlockingQueue
s'assure que tout ce que les threads ont fait avant de mettre des éléments dans la file d'attente est visible pour les threads qui retirent ces éléments.
Exécuteurs
Executor
Les s sont comme des s prêts à l'emploi BlockingQueue
qui exécutent des tâches. Exemple:
// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };
// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish
Pour plus de détails voir le doc pour Executor
, ExecutorService
et Executors
.
Gestion des événements
Faire une boucle en attendant que l'utilisateur clique sur quelque chose dans une interface utilisateur est incorrect. Utilisez plutôt les fonctionnalités de gestion des événements de la boîte à outils de l'interface utilisateur. Dans Swing , par exemple:
JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
// This event listener is run when the button is clicked.
// We don't need to loop while waiting.
label.setText("Button was clicked");
});
Étant donné que le gestionnaire d'événements s'exécute sur le thread de répartition des événements, un long travail dans le gestionnaire d'événements bloque toute autre interaction avec l'interface utilisateur jusqu'à ce que le travail soit terminé. Les opérations lentes peuvent être démarrées sur un nouveau thread ou distribuées vers un thread en attente à l'aide de l'une des techniques ci-dessus (attendre / notifier, a BlockingQueue
ou Executor
). Vous pouvez également utiliser a SwingWorker
, qui est conçu exactement pour cela, et fournit automatiquement un thread de travail en arrière-plan:
JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");
// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {
// Defines MyWorker as a SwingWorker whose result type is String:
class MyWorker extends SwingWorker<String,Void> {
@Override
public String doInBackground() throws Exception {
// This method is called on a background thread.
// You can do long work here without blocking the UI.
// This is just an example:
Thread.sleep(5000);
return "Answer is 42";
}
@Override
protected void done() {
// This method is called on the Swing thread once the work is done
String result;
try {
result = get();
} catch (Exception e) {
throw new RuntimeException(e);
}
label.setText(result); // will display "Answer is 42"
}
}
// Start the worker
new MyWorker().execute();
});
Minuteries
Pour effectuer des actions périodiques, vous pouvez utiliser un fichier java.util.Timer
. C'est plus facile à utiliser que d'écrire votre propre boucle de synchronisation, et plus facile à démarrer et à arrêter. Cette démo imprime l'heure actuelle une fois par seconde:
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
};
timer.scheduleAtFixedRate(task, 0, 1000);
Chacun java.util.Timer
a son propre thread d'arrière-plan qui est utilisé pour exécuter ses TimerTask
s programmés . Naturellement, le thread dort entre les tâches, il ne monopolise donc pas le processeur.
Dans le code Swing, il existe également un javax.swing.Timer
, qui est similaire, mais il exécute l'écouteur sur le thread Swing, afin que vous puissiez interagir en toute sécurité avec les composants Swing sans avoir à changer manuellement de thread:
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Timer timer = new Timer(1000, (ActionEvent e) -> {
frame.setTitle(String.valueOf(System.currentTimeMillis()));
});
timer.setRepeats(true);
timer.start();
frame.setVisible(true);
D'autres moyens
Si vous écrivez du code multithread, il vaut la peine d'explorer les classes de ces packages pour voir ce qui est disponible:
Et consultez également la section Concurrence des didacticiels Java. Le multithreading est compliqué, mais il y a beaucoup d'aide disponible!