web-dev-qa-db-fra.com

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

Dans mon code, j'ai une boucle qui attend qu'un état soit changé à partir d'un thread différent. L'autre thread fonctionne, mais ma boucle ne voit jamais la valeur modifiée. Cela attend indéfiniment. Cependant, quand je mets une instruction System.out.println Dans la boucle, ça marche 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 variable pizzaArrived. Mais la boucle ne fonctionne que lorsque je décommente l'instruction System.out.println("waiting");. Que se passe-t-il?

82
Boann

La JVM est autorisée à supposer que les autres threads ne modifient pas la variable pizzaArrived pendant la boucle. En d'autres termes, il peut hisser le test pizzaArrived == false En dehors de la boucle, optimisant ceci:

while (pizzaArrived == false) {}

en cela:

if (pizzaArrived == false) while (true) {}

qui est une boucle infinie.

Pour vous assurer que les modifications apportées par un thread sont visibles pour les autres threads, vous devez toujours ajouter une synchronisation entre les threads. La façon la plus simple de le faire est de rendre la variable partagée volatile:

volatile boolean pizzaArrived = false;

Faire une variable volatile garantit que différents threads verront les effets des modifications de chacun. Cela empêche la JVM de mettre en cache la valeur de 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 arrive-avant entre les accès à la variable. Cela signifie que tous les autres travaux effectués par un thread avant de livrer la pizza est également visible par le thread qui reçoit la pizza, même si ces autres modifications ne sont pas des variables volatile.)

Méthodes synchronisées sont principalement utilisées pour implémenter l'exclusion mutuelle (empêchant deux choses de se produire en même temps), mais elles ont également les mêmes effets secondaires que volatile. Les utiliser lors de la lecture et de l'écriture d'une variable est une autre façon de rendre les modifications visibles pour les 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 objet PrintStream. Les méthodes de PrintStream sont synchronisées comme ceci:

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

La synchronisation empêche pizzaArrived d'être mis en cache pendant la boucle. À proprement parler, les deux threads doivent se synchroniser sur le même objet pour garantir que les modifications apportées à la variable sont visibles. (Par exemple, appeler println après avoir défini pizzaArrived et le rappeler avant de lire 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 suffisamment intelligente pour prouver que d'autres threads n'appelleront pas println après avoir défini pizzaArrived, elle suppose donc qu'ils le pourraient. Par conséquent, il ne peut pas mettre en cache la variable pendant la boucle si vous appelez System.out.println. C'est pourquoi les boucles comme celle-ci fonctionnent lorsqu'elles ont une instruction print, bien que ce ne soit pas une correction correcte.

Utiliser 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! En attendant, 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 boucle se mette en attente pendant qu'il n'attrape pas le CPU.

Voici quelques façons de procéder:

Utilisation de l'attente/notification

Une solution de bas niveau consiste à tilisez les méthodes wait/notify de Object :

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 CPU pendant le sommeil. Une fois que le deuxième thread a défini la variable, il appelle notifyAll() pour réveiller tous les threads qui attendaient cet objet. C'est comme si le gars de la pizza sonne à la porte, vous pouvez donc vous asseoir et vous reposer en attendant, au lieu de vous tenir maladroitement devant 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 de l'objectif de la synchronisation), mais cela fonctionne ici car un thread libère temporairement le verrou de synchronisation lorsqu'il se trouve dans le wait() méthode.

BlockingQueue

Un BlockingQueue est utilisé pour implémenter les files d'attente producteur-consommateur. Les "consommateurs" prennent les articles à l'avant de la file d'attente et les "producteurs" poussent les articles à 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 générer InterruptedExceptions, qui sont des exceptions vérifiées qui doivent être gérées. Dans le code ci-dessus, par souci 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, BlockingQueue est très facile à utiliser.

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

Executors sont comme des BlockingQueues prêts à l'emploi 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, consultez le document 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. In 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 travail long dans le gestionnaire d'événements bloque toute autre interaction avec l'interface utilisateur jusqu'à la fin du travail. Les opérations lentes peuvent être démarrées sur un nouveau thread ou envoyées à un thread en attente à l'aide de l'une des techniques ci-dessus (attendre/notifier, un BlockingQueue ou Executor). Vous pouvez également utiliser un 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 Java.util.Timer . Il 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);

Chaque Java.util.Timer A son propre thread d'arrière-plan qui est utilisé pour exécuter ses TimerTasks planifiés. Naturellement, le thread dort entre les tâches, donc il ne monopolise pas le CPU.

Dans le code Swing, il y a aussi javax.swing.Timer , qui est similaire, mais il exécute l'écouteur sur le thread Swing, de sorte que vous pouvez interagir en toute sécurité avec les composants Swing sans avoir à changer manuellement de threads :

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 voir aussi section Concurrence des Java tutoriels. Le multithreading est compliqué, mais il y a beaucoup d'aide disponible!

131
Boann