JavaRush /Java-Blog /Random-DE /Sie können Java nicht mit einem Thread verderben: Teil II...
Viacheslav
Level 3

Sie können Java nicht mit einem Thread verderben: Teil III – Interaktion

Veröffentlicht in der Gruppe Random-DE
Ein kurzer Überblick über die Funktionen der Thread-Interaktion. Zuvor haben wir uns angeschaut, wie Threads miteinander synchronisiert werden. Dieses Mal gehen wir auf die Probleme ein, die bei der Interaktion von Threads auftreten können, und sprechen darüber, wie sie vermieden werden können. Wir werden auch einige nützliche Links für tiefergehende Studien bereitstellen. Mit einem Thread kann man Java nicht ruinieren: Teil III – Interaktion – 1

Einführung

Wir wissen also, dass es Threads in Java gibt, worüber Sie im Testbericht „ Thread Can’t Spoil Java: Part I – Threads “ nachlesen können und dass Threads miteinander synchronisiert werden können, was wir im Testbericht behandelt haben. Thread kann Java nicht verderben „Verderben: Teil II – Synchronisierung “. Es ist Zeit, darüber zu sprechen, wie Threads miteinander interagieren. Wie teilen sie gemeinsame Ressourcen? Welche Probleme könnte es dabei geben?

Sackgasse

Das schlimmste Problem ist Deadlock. Wenn zwei oder mehr Threads ewig aufeinander warten, spricht man von Deadlock. Nehmen wir ein Beispiel von der Oracle-Website aus der Beschreibung des Konzepts „ 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();
    }
}
Der Deadlock tritt hier möglicherweise nicht beim ersten Mal auf, aber wenn die Ausführung Ihres Programms hängen bleibt, ist es Zeit zum Ausführen jvisualvm: Mit einem Thread kann man Java nicht ruinieren: Teil III – Interaktion – 2Wenn ein Plugin in JVisualVM installiert ist (über Tools -> Plugins), können wir sehen, wo der Deadlock aufgetreten ist:
"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
Thread 1 wartet auf eine Sperre von Thread 0. Warum passiert das? Thread-1startet die Ausführung und führt die Methode aus Friend#bow. Es ist mit dem Schlüsselwort gekennzeichnet synchronized, d. h. wir holen den Monitor ab this. Am Eingang der Methode erhielten wir einen Link zu einer anderen Friend. Nun Thread-1möchte der Thread eine Methode auf einem anderen ausführen Friendund erhält dadurch ebenfalls eine Sperre von ihm. Aber wenn es einem anderen Thread (in diesem Fall Thread-0) gelungen ist, in die Methode einzudringen bow, ist die Sperre bereits beschäftigt und Thread-1wartet Thread-0und umgekehrt. Die Blockierung ist unlösbar, also ist sie tot, also tot. Sowohl ein Todesgriff (der nicht gelöst werden kann) als auch ein toter Block, aus dem man nicht entkommen kann. Zum Thema Deadlock können Sie sich das Video ansehen: „ Deadlock – Concurrency #1 – Advanced Java “.

Livelock

Wenn es einen Deadlock gibt, gibt es dann einen Livelock? Ja, das gibt es) Livelock bedeutet, dass Threads äußerlich lebendig zu sein scheinen, gleichzeitig aber nichts tun können, weil ... die Bedingung, unter der sie versuchen, ihre Arbeit fortzusetzen, kann nicht erfüllt werden. Im Wesentlichen ähnelt Livelock einem Deadlock, aber die Threads „hängen“ nicht auf dem System und warten auf den Monitor, sondern tun immer etwas. Zum Beispiel:
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();
    }
}
Der Erfolg dieses Codes hängt von der Reihenfolge ab, in der der Java-Thread-Scheduler die Threads startet. Wenn es zuerst startet Thead-1, erhalten wir 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
...
Wie aus dem Beispiel hervorgeht, versuchen beide Threads abwechselnd, beide Sperren zu erfassen, was jedoch fehlschlägt. Außerdem sind sie nicht in einer Sackgasse, das heißt, optisch ist bei ihnen alles in Ordnung und sie machen ihren Job. Mit einem Thread kann man Java nicht ruinieren: Teil III – Interaktion – 3Laut JVisualVM sehen wir die Schlafperioden und die Parkperiode (das heißt, wenn ein Thread versucht, eine Sperre zu besetzen, geht er in den Parkzustand über, wie wir zuvor bei der Thread-Synchronisierung besprochen haben ). Zum Thema Livelock sehen Sie ein Beispiel: „ Java – Thread Livelock “.

Hunger

Neben dem Blockieren (Deadlock und Livelock) gibt es bei der Arbeit mit Multithreading ein weiteres Problem: Starvation oder „Starvation“. Dieses Phänomen unterscheidet sich vom Blockieren dadurch, dass die Threads nicht blockiert werden, sondern einfach nicht über genügend Ressourcen für alle verfügen. Während daher einige Threads die gesamte Ausführungszeit in Anspruch nehmen, können andere nicht ausgeführt werden: Mit einem Thread kann man Java nicht ruinieren: Teil III – Interaktion – 4

https://www.logicbig.com/

Ein super Beispiel finden Sie hier: „ Java – Thread Starvation and Fairness “. Dieses Beispiel zeigt, wie Threads in Starvation funktionieren und wie eine kleine Änderung von Thread.sleep zu Thread.wait die Last gleichmäßig verteilen kann. Mit einem Thread kann man Java nicht ruinieren: Teil III – Interaktion – 5

Rennbedingung

Bei der Arbeit mit Multithreading gibt es so etwas wie eine „Race Condition“. Dieses Phänomen liegt darin begründet, dass Threads eine bestimmte Ressource untereinander teilen und der Code so geschrieben ist, dass er in diesem Fall keinen korrekten Betrieb gewährleistet. Schauen wir uns ein Beispiel an:
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();
    }
}
Dieser Code generiert möglicherweise beim ersten Mal keinen Fehler. Und es könnte so aussehen:
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)
Wie Sie sehen können, ist bei der Zuweisung newValueetwas schiefgegangen und newValuees gab noch mehr. Einige der Threads im Rennzustand haben es geschafft, valuezwischen diesen beiden Teams zu wechseln. Wie wir sehen können, ist ein Wettlauf zwischen den Threads entstanden. Stellen Sie sich nun vor, wie wichtig es ist, bei Geldtransaktionen keine ähnlichen Fehler zu machen ... Beispiele und Diagramme finden Sie auch hier: „ Code zur Simulation von Race Conditions in Java Thread “.

Flüchtig

Wenn es um die Interaktion von Threads geht, ist das Schlüsselwort besonders hervorzuheben volatile. Schauen wir uns ein einfaches Beispiel an:
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;
    }
}
Das Interessanteste ist, dass es mit hoher Wahrscheinlichkeit nicht funktionieren wird. Der neue Thread wird die Änderung nicht sehen flag. Um dies zu beheben, flagmüssen Sie ein Schlüsselwort für das Feld angeben volatile. Wie und warum? Alle Aktionen werden vom Prozessor ausgeführt. Die Berechnungsergebnisse müssen jedoch irgendwo gespeichert werden. Zu diesem Zweck gibt es auf dem Prozessor Arbeitsspeicher und einen Hardware-Cache. Diese Prozessor-Caches sind wie ein kleines Stück Speicher, um schneller auf Daten zuzugreifen als auf den Hauptspeicher. Aber alles hat auch eine Kehrseite: Die Daten im Cache sind möglicherweise nicht aktuell (wie im obigen Beispiel, als der Flag-Wert nicht aktualisiert wurde). Das Schlüsselwort teilt der JVM also volatilemit, dass wir unsere Variable nicht zwischenspeichern möchten. Dadurch können Sie das tatsächliche Ergebnis in allen Threads sehen. Dies ist eine sehr vereinfachte Formulierung. Zu diesem Thema volatilewird dringend empfohlen, die Übersetzung von „ JSR 133 (Java Memory Model) FAQ “ zu lesen. Ich empfehle Ihnen außerdem, mehr über die Materialien „ Java Memory Model “ und „ Java Volatile Keyword “ zu lesen. Darüber hinaus ist es wichtig zu bedenken, dass es volatilehier um Sichtbarkeit und nicht um die Atomizität von Änderungen geht. Wenn wir den Code aus „Race Condition“ übernehmen, sehen wir einen Hinweis in IntelliJ Idea: Mit einem Thread kann man Java nicht ruinieren: Teil III – Interaktion – 6Diese Inspektion (Inspection) wurde zu IntelliJ Idea als Teil des Problems IDEA-61117 hinzugefügt, das bereits 2010 in den Versionshinweisen aufgeführt war .

Atomarität

Atomare Operationen sind Operationen, die nicht geteilt werden können. Beispielsweise ist die Operation, einer Variablen einen Wert zuzuweisen, atomar. Leider ist Inkrementieren keine atomare Operation, weil Für eine Erhöhung sind bis zu drei Vorgänge erforderlich: Den alten Wert abrufen, einen Wert hinzufügen und den Wert speichern. Warum ist Atomizität wichtig? Wenn im Inkrementierungsbeispiel eine Race-Bedingung auftritt, kann sich die gemeinsam genutzte Ressource (d. h. der gemeinsam genutzte Wert) jederzeit plötzlich ändern. Darüber hinaus ist es wichtig, dass auch 64-Bit-Strukturen nicht atomar sind, zum Beispiel longund double. Weitere Informationen finden Sie hier: „ Atomizität beim Lesen und Schreiben von 64-Bit-Werten sicherstellen “. Ein Beispiel für Probleme mit der Atomizität ist im folgenden Beispiel zu sehen:
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());
    }
}
Eine spezielle Klasse für die Arbeit mit Atomic Integerzeigt uns immer 30000 an, aber valuedas wird sich von Zeit zu Zeit ändern. Zu diesem Thema gibt es einen kurzen Überblick „ An Introduction to Atomic Variables in Java “. Atomic basiert auf dem Compare-and-Swap-Algorithmus. Mehr dazu lesen Sie im Artikel auf Habré „ Vergleich lockfreier Algorithmen – CAS und FAA am Beispiel von JDK 7 und 8 “ oder auf Wikipedia im Artikel über „ Vergleich mit Austausch “. Mit einem Thread kann man Java nicht ruinieren: Teil III – Interaktion – 8

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

Passiert vorher

Es gibt eine interessante und mysteriöse Sache – Happens Before. Apropos Flows: Es lohnt sich auch, darüber zu lesen. Die Beziehung „Happens Before“ gibt die Reihenfolge an, in der Aktionen zwischen Threads angezeigt werden. Es gibt viele Interpretationen und Interpretationen. Einer der aktuellsten Berichte zu diesem Thema ist dieser Bericht:
Es ist wahrscheinlich besser, dass dieses Video nichts darüber erzählt. Deshalb hinterlasse ich einfach einen Link zum Video. Sie können „ Java – Happens-before-Beziehungen verstehen “ lesen.

Ergebnisse

In diesem Test haben wir uns die Funktionen der Thread-Interaktion angesehen. Wir besprachen mögliche auftretende Probleme und Möglichkeiten, diese zu erkennen und zu beseitigen. Liste zusätzlicher Materialien zum Thema: #Wjatscheslaw
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION