JavaRush /Blog Java /Random-FR /Vous ne pouvez pas gâcher Java avec un fil de discussion ...
Viacheslav
Niveau 3

Vous ne pouvez pas gâcher Java avec un fil de discussion : partie III – Interaction

Publié dans le groupe Random-FR
Un bref aperçu des fonctionnalités de l'interaction avec les threads. Auparavant, nous avons examiné comment les threads se synchronisaient les uns avec les autres. Cette fois, nous allons nous pencher sur les problèmes qui peuvent survenir lorsque les threads interagissent et expliquer comment ils peuvent être évités. Nous fournirons également quelques liens utiles pour une étude plus approfondie. Vous ne pouvez pas ruiner Java avec un thread : Partie III - interaction - 1

Introduction

Ainsi, nous savons qu'il existe des threads en Java, que vous pouvez lire dans la revue « Thread Can't Spoil Java: Part I - Threads » et que les threads peuvent être synchronisés les uns avec les autres, ce que nous avons traité dans la revue « Le fil de discussion ne peut pas gâcher Java « Spoil : Partie II - Synchronisation ». Il est temps de parler de la façon dont les fils interagissent les uns avec les autres. Comment partagent-ils des ressources communes ? Quels problèmes pourrait-il y avoir avec cela ?

Impasse

Le pire problème est l’impasse. Lorsque deux threads ou plus s'attendent indéfiniment, cela s'appelle Deadlock. Prenons un exemple du site Oracle à partir de la description du concept de « Deadlock » :

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 has bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s has 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 ici n'apparaîtra peut-être pas du premier coup, mais si l'exécution de votre programme est bloquée, il est temps de l'exécuter jvisualvm: Vous ne pouvez pas ruiner Java avec un thread : Partie III - interaction - 2Si un plugin est installé dans JVisualVM (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 &lt33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Le thread 1 attend un verrou du thread 0. Pourquoi cela se produit-il ? Thread-1démarre l'exécution et exécute la méthode Friend#bow. Il est marqué du mot-clé synchronized, c'est-à-dire que nous récupérons le moniteur par this. A l'entrée de la méthode, nous avons reçu un lien vers une autre Friend. Maintenant, le thread Thread-1veut exécuter une méthode sur un autre Friend, obtenant ainsi également un verrou de sa part. Mais si un autre thread (dans ce cas Thread-0) parvient à entrer dans la méthode bow, alors le verrou est déjà occupé et Thread-1en attente Thread-0, et vice versa. Le blocage est insoluble, il est donc mort, c'est-à-dire mort. A la fois une emprise mortelle (qui ne peut être relâchée) et un bloc mort dont on ne peut s'échapper. Sur le thème du blocage, vous pouvez regarder la vidéo : " Deadlock - Concurrency #1 - Advanced Java ".

Livelock

S’il y a une impasse, y a-t-il un Livelock ? Oui, il y en a) Livelock, c'est que les fils semblent être vivants extérieurement, mais en même temps, ils ne peuvent rien faire, parce que... la condition dans laquelle ils tentent de poursuivre leur travail ne peut être remplie. Essentiellement, Livelock est similaire à un blocage, mais les threads ne « se bloquent » pas sur le système en attendant le moniteur, mais 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. S'il démarre en premier Thead-1, nous obtiendrons 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 le montre l'exemple, les deux threads tentent alternativement de capturer les deux verrous, mais ils échouent. De plus, ils ne sont pas dans une impasse, c'est-à-dire que visuellement tout va bien pour eux et qu'ils font leur travail. Vous ne pouvez pas ruiner Java avec un thread : Partie III - interaction - 3Selon JVisualVM, nous voyons les périodes de sommeil et la période de stationnement (c'est-à-dire lorsqu'un thread tente d'occuper un verrou, il passe à l'état de parc, comme nous l'avons évoqué plus tôt en parlant de synchronisation des threads ). Au sujet du livelock, vous pouvez voir un exemple : " Java - Thread Livelock ".

famine

En plus du blocage (deadlock et livelock), il existe un autre problème lorsque l'on travaille avec le multithreading - la famine, ou « famine ». Ce phénomène diffère du blocage dans la mesure où les threads ne sont pas bloqués, mais ils n'ont tout simplement pas assez de ressources pour tout le monde. Ainsi, même si certains threads prennent tout le temps d’exécution, d’autres ne peuvent pas être exécutés : Vous ne pouvez pas ruiner Java avec un thread : Partie III - interaction - 4

https://www.logicbig.com/

Un super exemple peut être trouvé ici : " Java - Thread Starvation and Fairness ". Cet exemple montre comment les threads fonctionnent dans Starvation et comment un petit changement de Thread.sleep à Thread.wait peut répartir la charge uniformément. Vous ne pouvez pas ruiner Java avec un thread : Partie III - interaction - 5

Condition de course

Lorsque vous travaillez avec le multithreading, il existe une « condition de concurrence critique ». Ce phénomène réside dans le fait que les threads partagent une certaine ressource entre eux et que le code est écrit de telle manière qu'il ne permet pas un fonctionnement correct dans ce cas. Regardons 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. Et cela pourrait 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 constater, lors de son attribution, newValuequelque chose s'est mal passé et newValueil y en a eu d'autres. Certains fils de l’état de course ont réussi à changer valueentre ces deux équipes. Comme on peut le constater, une course entre les fils est apparue. Imaginez maintenant à quel point il est important de ne pas commettre des erreurs similaires avec les transactions monétaires... Des exemples et des diagrammes peuvent également être trouvés ici : « Code pour simuler une condition de concurrence dans un thread Java ».

Volatil

En parlant de l'interaction des threads, il convient particulièrement de noter le mot-clé volatile. Regardons 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;
    }
}
La chose la plus intéressante est qu'avec un degré de probabilité élevé, cela ne fonctionnera pas. Le nouveau fil ne verra pas le changement flag. Pour résoudre ce problème, flagvous devez spécifier un mot-clé pour le champ volatile. Comment et pourquoi? Toutes les actions sont effectuées par le processeur. Mais les résultats des calculs doivent être stockés quelque part. A cet effet, le processeur dispose d'une mémoire principale et d'un cache matériel. Ces caches de processeur sont comme un petit morceau de mémoire permettant d'accéder aux données plus rapidement que d'accéder à la mémoire principale. Mais tout a aussi un inconvénient : les données dans le cache peuvent ne pas être à jour (comme dans l'exemple ci-dessus, lorsque la valeur du drapeau n'a pas été mise à jour). Ainsi, le mot-clé volatileindique à la JVM que nous ne voulons pas mettre en cache notre variable. Cela vous permet de voir le résultat réel dans tous les threads. Il s'agit d'une formulation très simplifiée. A ce sujet, volatileil est fortement recommandé de lire la traduction de la « FAQ JSR 133 (Java Memory Model) ». Je vous conseille également d'en savoir plus sur les documents « Java Memory Model » et « Java Volatile Keyword ». En outre, il est important de rappeler qu’il volatiles’agit ici de visibilité et non d’atomicité des changements. Si nous prenons le code de "Race Condition", nous verrons un indice dans IntelliJ Idea : Vous ne pouvez pas ruiner Java avec un thread : Partie III - interaction - 6cette inspection (Inspection) a été ajoutée à IntelliJ Idea dans le cadre du problème IDEA-61117 , qui était répertorié dans les notes de version en 2010.

Atomicité

Les opérations atomiques sont des opérations qui ne peuvent être divisées. Par exemple, l’opération consistant à attribuer une valeur à une variable est atomique. Malheureusement, l’incrémentation n’est pas une opération atomique, car un incrément nécessite jusqu'à trois opérations : récupérer l'ancienne valeur, y ajouter une et enregistrer la valeur. Pourquoi l’atomicité est-elle importante ? Dans l'exemple d'incrémentation, si une condition de concurrence critique se produit, à tout moment la ressource partagée (c'est-à-dire la valeur partagée) peut soudainement changer. De plus, il est important que les structures 64 bits ne soient pas non plus atomiques, par exemple longet double. Vous pouvez en savoir plus ici : " Assurer l'atomicité lors de la lecture et de l'écriture de valeurs 64 bits ". Un exemple de problèmes liés à l'atomicité peut être vu dans l'exemple suivant :

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());
    }
}
Une classe spéciale pour travailler avec atomique Integernous montrera toujours 30 000, mais valuecela changera de temps en temps. Il y a un bref aperçu sur ce sujet " Une introduction aux variables atomiques en Java ". Atomic est basé sur l'algorithme Compare-and-Swap. Vous pouvez en savoir plus à ce sujet dans l'article sur Habré « Comparaison des algorithmes sans verrouillage - CAS et FAA en utilisant l'exemple du JDK 7 et 8 » ou sur Wikipédia dans l'article « Comparaison avec échange ». Vous ne pouvez pas ruiner Java avec un thread : Partie III - interaction - 8

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

Cela arrive avant

Il y a une chose intéressante et mystérieuse - qui se produit avant. En parlant de flux, cela vaut également la peine d’être lu à ce sujet. La relation Happens Before indique l’ordre dans lequel les actions entre les threads seront vues. Il existe de nombreuses interprétations et interprétations. L'un des rapports les plus récents sur ce sujet est le suivant :
Il vaut probablement mieux que cette vidéo n’en dise rien. Je vais donc juste laisser un lien vers la vidéo. Vous pouvez lire " Java - Comprendre les relations qui se produisent avant ".

Résultats

Dans cette revue, nous avons examiné les fonctionnalités de l'interaction avec les threads. Nous avons discuté des problèmes qui peuvent survenir et des moyens de les détecter et de les éliminer. Liste de documents supplémentaires sur le sujet : #Viacheslav
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION