La boucle ne voit pas la valeur modifiée par un autre thread sans instruction d'impression


89

Dans mon code, j'ai une boucle qui attend qu'un état soit changé à partir d'un autre thread. L'autre thread fonctionne, mais ma boucle ne voit jamais la valeur modifiée. Il attend pour toujours. Cependant, quand je mets une System.out.printlndéclaration dans la boucle, cela fonctionne soudainement! Pourquoi?


Voici un exemple de mon code:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (pizzaArrived == false) {
            //System.out.println("waiting");
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        pizzaArrived = true;
    }
}

Pendant que la boucle while est en cours d'exécution, j'appelle à deliverPizza()partir d'un thread différent pour définir la pizzaArrivedvariable. Mais la boucle ne fonctionne que lorsque je décommente l' System.out.println("waiting");instruction. Que se passe-t-il?

Réponses:


148

La JVM est autorisée à supposer que les autres threads ne modifient pas la pizzaArrivedvariable pendant la boucle. En d'autres termes, il peut hisser le pizzaArrived == falsetest 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 volatilegarantit que les différents threads verront les effets des changements de chacun. Cela empêche la JVM de mettre en cache la valeur pizzaArrivedou 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, volatilecré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 volatilevariables.)

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.outest un PrintStreamobjet. Les méthodes de PrintStreamsont synchronisées comme ceci:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

La synchronisation empêche la pizzaArrivedmise 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 printlnaprès la configuration pizzaArrivedet un nouvel appel avant la lecture pizzaArrivedserait 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 printlnaprè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.outn'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 BlockingQueueest 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 putet takede BlockingQueuepeuvent lancer des InterruptedExceptions, 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 BlockingQueueest très facile à utiliser.

Aucune autre synchronisation n'est nécessaire ici car a BlockingQueues'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

ExecutorLes s sont comme des s prêts à l'emploi BlockingQueuequi 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, ExecutorServiceet 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 BlockingQueueou 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.Timera son propre thread d'arrière-plan qui est utilisé pour exécuter ses TimerTasks 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!


Réponse très professionnelle, après avoir lu cette idée fausse, je vous remercie
Humoyun Ahmad

1
Réponse géniale. Je travaille avec les threads Java depuis un bon moment et j'ai encore appris quelque chose ici ( wait()libère le verrou de synchronisation!).
brimborium

Merci Boann! Bonne réponse, c'est comme un article complet avec des exemples! Oui, aussi aimé "wait () libère le verrou de synchronisation"
Kiryl Ivanou

java public class ThreadTest { private static boolean flag = false; private static class Reader extends Thread { @Override public void run() { while(flag == false) {} System.out.println(flag); } } public static void main(String[] args) { new Reader().start(); flag = true; } } @Boann, ce code ne hisse pas le pizzaArrived == falsetest en dehors de la boucle, et la boucle peut voir le drapeau changé par le thread principal, pourquoi?
gaussclb

1
@gaussclb Si vous voulez dire que vous avez décompilé un fichier de classe, corrigez. Le compilateur Java n'effectue pratiquement aucune optimisation. Le levage est effectué par la JVM. Vous devez démonter le code machine natif. Essayez: wiki.openjdk.java.net/display/HotSpot/PrintAssembly
Boann
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.