CodeGym/Blog Java/Random-FR/Mieux ensemble : Java et la classe Thread. Partie III — I...
John Squirrels
Niveau 41
San Francisco

Mieux ensemble : Java et la classe Thread. Partie III — Interaction

Publié dans le groupe Random-FR
membres
Un bref aperçu des détails de la façon dont les threads interagissent. Auparavant, nous avons examiné comment les threads sont synchronisés les uns avec les autres. Cette fois, nous allons plonger dans les problèmes qui peuvent survenir lorsque les threads interagissent, et nous verrons comment les éviter. Nous fournirons également quelques liens utiles pour une étude plus approfondie. Mieux ensemble : Java et la classe Thread.  Partie III — Interaction - 1

Introduction

Donc, nous savons que Java a des threads. Vous pouvez lire à ce sujet dans la revue intitulée Mieux ensemble : Java et la classe Thread. Partie I — Threads d'exécution . Et nous avons exploré le fait que les threads peuvent se synchroniser entre eux dans la revue intitulée Better together : Java and the Thread class. Partie II — Synchronisation . Il est temps de parler de la façon dont les threads interagissent les uns avec les autres. Comment partagent-ils les ressources partagées ? Quels problèmes pourraient survenir ici? Mieux ensemble : Java et la classe Thread.  Partie III — Interaction - 2

Impasse

Le problème le plus effrayant de tous est l'impasse. L'impasse est lorsque deux threads ou plus attendent éternellement l'autre. Nous prendrons un exemple de la page Web d'Oracle qui décrit le blocage :
public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s bowed back to me!%n",
                    this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(() -> alphonse.bow(gaston)).start();
        new Thread(() -> gaston.bow(alphonse)).start();
    }
}
Le blocage peut ne pas se produire ici la première fois, mais si votre programme se bloque, alors il est temps de s'exécuter jvisualvm: Mieux ensemble : Java et la classe Thread.  Partie III — Interaction - 3Avec un plugin JVisualVM installé (via Outils -> Plugins), nous pouvons voir où le blocage s'est produit :
"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
	at Deadlock$Friend.bowBack(Deadlock.java:16)
	- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Le thread 1 attend le verrou du thread 0. Pourquoi cela se produit-il ? Thread-1démarre et exécute la Friend#bowméthode. Il est marqué du synchronizedmot-clé, ce qui signifie que nous acquérons le moniteur pour this(l'objet actuel). L'entrée de la méthode était une référence à l'autre Friendobjet. Maintenant, Thread-1veut exécuter la méthode sur l'autre Friendet doit acquérir son verrou pour ce faire. Mais si l'autre thread (dans ce cas Thread-0) a réussi à entrer dans la bow()méthode, alors le verrou a déjà été acquis et Thread-1attendThread-0, et vice versa. Cette impasse est insoluble, et nous l'appelons impasse. Comme une emprise mortelle qui ne peut être relâchée, l'impasse est un blocage mutuel qui ne peut être brisé. Pour une autre explication de l'impasse, vous pouvez regarder cette vidéo : Deadlock et Livelock expliqués .

Livelock

S'il y a impasse, y a-t-il aussi livelock ? Oui, il y a :) Livelock se produit lorsque les threads semblent être vivants, mais qu'ils sont incapables de faire quoi que ce soit, car la ou les conditions requises pour qu'ils continuent leur travail ne peuvent pas être remplies. Fondamentalement, le livelock est similaire au blocage, mais les threads ne "se bloquent" pas en attendant un moniteur. Au lieu de cela, ils font toujours quelque chose. Par exemple:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App {
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";

    public static void log(String text) {
        String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
        String color = ANSI_BLUE;
        int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
        if (val != 0) {
            color = ANSI_PURPLE;
        }
        System.out.println(color + name + ": " + text + color);
        try {
            System.out.println(color + name + ": wait for " + val + " sec" + color);
            Thread.currentThread().sleep(val * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Lock first = new ReentrantLock();
        Lock second = new ReentrantLock();

        Runnable locker = () -> {
            boolean firstLocked = false;
            boolean secondLocked = false;
            try {
                while (!firstLocked || !secondLocked) {
                    firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
                    log("First Locked: " + firstLocked);
                    secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
                    log("Second Locked: " + secondLocked);
                }
                first.unlock();
                second.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(locker).start();
        new Thread(locker).start();
    }
}
Le succès de ce code dépend de l'ordre dans lequel le planificateur de threads Java démarre les threads. Si Thead-1commence en premier, alors nous obtenons livelock :
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Comme vous pouvez le voir dans l'exemple, les deux threads tentent d'acquérir les deux verrous à tour de rôle, mais ils échouent. Mais, ils ne sont pas dans l'impasse. Extérieurement, tout va bien et ils font leur travail. Mieux ensemble : Java et la classe Thread.  Partie III — Interaction - 4Selon JVisualVM, nous voyons des périodes de veille et une période de parc (c'est-à-dire lorsqu'un thread tente d'acquérir un verrou — il entre dans l'état de parc, comme nous l'avons vu précédemment lorsque nous avons parlé de la synchronisation des threads ) . Vous pouvez voir un exemple de livelock ici : Java - Thread Livelock .

famine

En plus du blocage et du livelock, il existe un autre problème qui peut survenir lors du multithreading : la famine. Ce phénomène diffère des formes de blocage précédentes en ce que les threads ne sont pas bloqués - ils n'ont tout simplement pas les ressources suffisantes. Par conséquent, alors que certains threads prennent tout le temps d'exécution, d'autres sont incapables de s'exécuter : Mieux ensemble : Java et la classe Thread.  Partie III — Interaction - 5

https://www.logicbig.com/

Vous pouvez voir un super exemple ici : Java - Thread Starvation and Fairness . Cet exemple montre ce qui se passe avec les threads pendant la famine et comment un petit changement de à Thread.sleep()vous Thread.wait()permet de répartir la charge uniformément. Mieux ensemble : Java et la classe Thread.  Partie III — Interaction - 6

Conditions de course

Dans le multithreading, il existe une "condition de concurrence". Ce phénomène se produit lorsque des threads partagent une ressource, mais que le code est écrit de manière à ne pas garantir un partage correct. Jetez un oeil à un exemple:
public class App {
    public static int value = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                int oldValue = value;
                int newValue = ++value;
                if (oldValue + 1 != newValue) {
                    throw new IllegalStateException(oldValue + " + 1 = " + newValue);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}
Ce code peut ne pas générer d'erreur la première fois. Quand c'est le cas, cela peut ressembler à ceci:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
	at App.lambda$main$0(App.java:13)
	at java.lang.Thread.run(Thread.java:745)
Comme vous pouvez le voir, quelque chose s'est mal passé lors newValuede l'attribution d'une valeur. newValueC est trop gros. En raison de la condition de concurrence, l'un des threads a réussi à modifier la variable valueentre les deux instructions. Il s'avère qu'il y a une course entre les fils. Pensez maintenant à quel point il est important de ne pas commettre d'erreurs similaires avec des transactions monétaires... Des exemples et des diagrammes peuvent également être vus ici : Code pour simuler la condition de concurrence dans le thread Java .

Volatil

Parlant de l'interaction des threads, le volatilemot-clé mérite d'être mentionné. Prenons un exemple simple :
public class App {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable whileFlagFalse = () -> {
            while(!flag) {
            }
            System.out.println("Flag is now TRUE");
        };

        new Thread(whileFlagFalse).start();
        Thread.sleep(1000);
        flag = true;
    }
}
Plus intéressant encore, il est fort probable que cela ne fonctionne pas. Le nouveau fil ne verra pas le changement dans le flagchamp. Pour résoudre ce problème pour le flagchamp, nous devons utiliser le volatilemot-clé. Comment et pourquoi? Le processeur effectue toutes les actions. Mais les résultats des calculs doivent être stockés quelque part. Pour cela, il y a la mémoire principale et il y a le cache du processeur. Les caches d'un processeur sont comme un petit morceau de mémoire utilisé pour accéder aux données plus rapidement que lors de l'accès à la mémoire principale. Mais tout a un revers : les données du cache peuvent ne pas être à jour (comme dans l'exemple ci-dessus, lorsque la valeur du champ flag n'a pas été mise à jour). Alors levolatileLe mot-clé indique à la JVM que nous ne voulons pas mettre en cache notre variable. Cela permet d'afficher le résultat à jour sur tous les threads. C'est une explication très simplifiée. Quant au volatilemot-clé, je vous recommande vivement de lire cet article . Pour plus d'informations, je vous conseille également de lire Java Memory Model et Java Volatile Keyword . De plus, il est important de se rappeler qu'il volatiles'agit de la visibilité, et non de l'atomicité des changements. En regardant le code dans la section "Conditions de course", nous verrons une info-bulle dans IntelliJ IDEA : Mieux ensemble : Java et la classe Thread.  Partie III — Interaction - 7cette inspection a été ajoutée à IntelliJ IDEA dans le cadre du problème IDEA-61117 , qui figurait dans les notes de publication en 2010.

Atomicité

Les opérations atomiques sont des opérations qui ne peuvent pas être divisées. Par exemple, l'opération d'assignation d'une valeur à une variable doit être atomique. Malheureusement, l'opération d'incrémentation n'est pas atomique, car l'incrémentation nécessite jusqu'à trois opérations CPU : obtenir l'ancienne valeur, en ajouter une, puis enregistrer la valeur. Pourquoi l'atomicité est-elle importante ? Avec l'opération d'incrémentation, s'il existe une condition de concurrence, la ressource partagée (c'est-à-dire la valeur partagée) peut soudainement changer à tout moment. De plus, les opérations impliquant des structures 64 bits, par exemple longet double, ne sont pas atomiques. Plus de détails peuvent être lus ici : Assurer l'atomicité lors de la lecture et de l'écriture de valeurs 64 bits . Les problèmes liés à l'atomicité peuvent être vus dans cet exemple :
public class App {
    public static int value = 0;
    public static AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                value++;
                atomic.incrementAndGet();
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
        Thread.sleep(300);
        System.out.println(value);
        System.out.println(atomic.get());
    }
}
La classe spéciale AtomicIntegernous donnera toujours 30 000, mais cela valuechangera de temps en temps. Il y a un bref aperçu de ce sujet : Introduction aux variables atomiques en Java . L'algorithme "comparer et échanger" est au cœur des classes atomiques. Vous pouvez en savoir plus à ce sujet ici dans Comparaison des algorithmes sans verrouillage - CAS et FAA sur l'exemple de JDK 7 et 8 ou dans l' article Compare-and-swap sur Wikipedia. Mieux ensemble : Java et la classe Thread.  Partie III — Interaction - 9

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

Se passe-avant

Il existe un concept intéressant et mystérieux appelé "se passe avant". Dans le cadre de votre étude des threads, vous devriez lire à ce sujet. La relation se produit avant montre l'ordre dans lequel les actions entre les threads seront vues. Il existe de nombreuses interprétations et commentaires. Voici l'une des présentations les plus récentes sur ce sujet : Java "Happens-Before" Relationships .

Résumé

Dans cette revue, nous avons exploré certaines des spécificités de l'interaction des threads. Nous avons discuté des problèmes qui peuvent survenir, ainsi que des moyens de les identifier et de les éliminer. Liste de documents supplémentaires sur le sujet : Mieux ensemble : Java et la classe Thread. Partie I — Threads d'exécution Mieux ensemble : Java et la classe Thread. Partie II — Synchronisation Mieux ensemble : Java et la classe Thread. Partie IV — Callable, Future et friends Mieux ensemble : Java et la classe Thread. Partie V — Executor, ThreadPool, Fork/Join Mieux ensemble : Java et la classe Thread. Partie VI — Tirez !
Commentaires
  • Populaires
  • Nouveau
  • Anciennes
Tu dois être connecté(e) pour laisser un commentaire
Cette page ne comporte pas encore de commentaires