CodeGym /Java-Blog /Random-DE /Besser zusammen: Java und die Thread-Klasse. Teil III – I...
John Squirrels
Level 41
San Francisco

Besser zusammen: Java und die Thread-Klasse. Teil III – Interaktion

Veröffentlicht in der Gruppe Random-DE
Ein kurzer Überblick über die Einzelheiten der Interaktion von Threads. Zuvor haben wir uns angeschaut, wie Threads miteinander synchronisiert werden. Dieses Mal werden wir uns mit den Problemen befassen, die bei der Interaktion von Threads auftreten können, und darüber sprechen, wie man sie vermeiden kann. Wir werden auch einige nützliche Links für tiefergehende Studien bereitstellen. Besser zusammen: Java und die Thread-Klasse.  Teil III – Interaktion – 1

Einführung

Wir wissen also, dass Java Threads hat. Darüber können Sie in der Rezension mit dem Titel „ Besser zusammen: Java und die Thread-Klasse“ nachlesen. Teil I – Threads zur Ausführung . Und die Tatsache, dass Threads miteinander synchronisiert werden können, haben wir in der Rezension mit dem Titel „ Besser zusammen: Java und die Thread-Klasse“ untersucht. Teil II – Synchronisierung . Es ist Zeit, darüber zu sprechen, wie Threads miteinander interagieren. Wie teilen sie gemeinsame Ressourcen? Welche Probleme könnten hier auftreten? Besser zusammen: Java und die Thread-Klasse.  Teil III – Interaktion – 2

Sackgasse

Das gruseligste Problem von allen ist der Deadlock. Deadlock liegt vor, wenn zwei oder mehr Threads ewig auf den anderen warten. Wir nehmen ein Beispiel von der Oracle-Webseite, das Deadlock beschreibt :

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();
    }
}
Möglicherweise tritt hier beim ersten Mal kein Deadlock auf, aber wenn Ihr Programm hängen bleibt, ist es an der Zeit, es auszuführen jvisualvm: Besser zusammen: Java und die Thread-Klasse.  Teil III – Interaktion – 3Wenn ein JVisualVM-Plugin installiert ist (über Extras -> 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 <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Thread 1 wartet auf die Sperre von Thread 0. Warum passiert das? Thread-1startet die Ausführung und führt die Friend#bowMethode aus. Es ist mit dem synchronizedSchlüsselwort gekennzeichnet, was bedeutet, dass wir den Monitor für (das aktuelle Objekt) erwerben this. Die Eingabe der Methode war ein Verweis auf das andere FriendObjekt. Möchte nun Thread-1die Methode auf dem anderen ausführen Friendund muss dazu dessen Sperre erwerben. Wenn es dem anderen Thread (in diesem Fall Thread-0) jedoch gelungen ist, die Methode einzugeben bow(), wurde die Sperre bereits erworben und Thread-1wartet daraufThread-0, und umgekehrt. Diese Sackgasse ist unlösbar und wir nennen sie Deadlock. Wie ein Todesgriff, der nicht gelöst werden kann, ist Deadlock eine gegenseitige Blockierung, die nicht durchbrochen werden kann. Für eine weitere Erklärung des Deadlocks können Sie sich dieses Video ansehen: Deadlock und Livelock erklärt .

Livelock

Wenn es einen Deadlock gibt, gibt es dann auch einen Livelock? Ja, das gibt es :) Livelock tritt auf, wenn Threads äußerlich lebendig zu sein scheinen, sie aber nichts tun können, weil die für die Fortsetzung ihrer Arbeit erforderlichen Bedingungen nicht erfüllt werden können. Grundsätzlich ähnelt Livelock einem Deadlock, aber die Threads „hängen“ nicht und warten auf einen Monitor. Stattdessen tun sie ständig 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 Thead-1zuerst gestartet wird, 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 Sie dem Beispiel entnehmen können, versuchen beide Threads nacheinander, beide Sperren zu erhalten, was jedoch fehlschlägt. Aber sie sind nicht in einer Sackgasse. Äußerlich ist alles in Ordnung und sie machen ihren Job. Besser zusammen: Java und die Thread-Klasse.  Teil III – Interaktion – 4Laut JVisualVM sehen wir Ruhephasen und eine Parkphase (das ist, wenn ein Thread versucht, eine Sperre zu erlangen – er wechselt in den Parkzustand, wie wir bereits früher besprochen haben, als wir über die Thread-Synchronisierung gesprochen haben ) . Ein Beispiel für Livelock können Sie hier sehen: Java - Thread Livelock .

Hunger

Zusätzlich zu Deadlock und Livelock gibt es ein weiteres Problem, das beim Multithreading auftreten kann: Hunger. Dieses Phänomen unterscheidet sich von den vorherigen Blockierungsformen dadurch, dass die Threads nicht blockiert werden – sie verfügen einfach nicht über ausreichende Ressourcen. Während einige Threads daher die gesamte Ausführungszeit in Anspruch nehmen, können andere nicht ausgeführt werden: Besser zusammen: Java und die Thread-Klasse.  Teil III – Interaktion – 5

https://www.logicbig.com/

Ein tolles Beispiel können Sie hier sehen: Java - Thread Starvation and Fairness . Dieses Beispiel zeigt, was mit Threads während des Hungerns passiert und wie Sie mit einer kleinen Änderung von Thread.sleep()auf Thread.wait()die Last gleichmäßig verteilen können. Besser zusammen: Java und die Thread-Klasse.  Teil III – Interaktion – 6

Rennbedingungen

Beim Multithreading gibt es so etwas wie eine „Race Condition“. Dieses Phänomen tritt auf, wenn Threads eine Ressource gemeinsam nutzen, der Code jedoch so geschrieben ist, dass eine korrekte gemeinsame Nutzung nicht gewährleistet ist. Schauen Sie sich 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. Wenn dies der Fall ist, könnte es 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, ist beim newValueZuweisen eines Werts ein Fehler aufgetreten. newValueist zu groß. Aufgrund der Race-Bedingung gelang es einem der Threads, die Variablen valuezwischen den beiden Anweisungen zu ändern. Es stellt sich heraus, dass es einen Wettlauf zwischen den Threads gibt. Denken Sie nun daran, wie wichtig es ist, bei Geldtransaktionen keine ähnlichen Fehler zu machen ... Beispiele und Diagramme sind auch hier zu sehen: Code zur Simulation von Race Conditions in Java Thread .

Flüchtig

Wenn es um die Interaktion von Threads geht, volatileist das Schlüsselwort erwähnenswert. 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;
    }
}
Am interessantesten ist, dass dies höchstwahrscheinlich nicht funktioniert. Der neue Thread wird die Änderung im flagFeld nicht sehen. Um dies für das flagFeld zu beheben, müssen wir das Schlüsselwort verwenden volatile. Wie und warum? Der Prozessor führt alle Aktionen aus. Aber die Ergebnisse der Berechnungen müssen irgendwo gespeichert werden. Dafür gibt es den Hauptspeicher und den Cache des Prozessors. Die Caches eines Prozessors sind wie ein kleiner Teil des Speichers, mit dem schneller auf Daten zugegriffen werden kann als beim Zugriff auf den Hauptspeicher. Aber alles hat einen Nachteil: Die Daten im Cache sind möglicherweise nicht aktuell (wie im obigen Beispiel, als der Wert des Flag-Felds nicht aktualisiert wurde). Also, dievolatileDas Schlüsselwort teilt der JVM mit, dass wir unsere Variable nicht zwischenspeichern möchten. Dadurch ist das aktuelle Ergebnis in allen Threads sichtbar. Dies ist eine stark vereinfachte Erklärung. Was das volatileSchlüsselwort betrifft, empfehle ich Ihnen dringend, diesen Artikel zu lesen . Für weitere Informationen empfehle ich Ihnen auch die Lektüre von Java Memory Model und Java Volatile Keyword . Darüber hinaus ist es wichtig, sich daran zu erinnern, dass volatilees um die Sichtbarkeit und nicht um die Atomizität von Änderungen geht. Wenn wir uns den Code im Abschnitt „Rennbedingungen“ ansehen, sehen wir einen Tooltip in IntelliJ IDEA: Besser zusammen: Java und die Thread-Klasse.  Teil III – Interaktion – 7Diese Inspektion 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 muss die Zuweisung eines Werts zu einer Variablen atomar sein. Leider ist die Inkrementierungsoperation nicht atomar, da die Inkrementierung bis zu drei CPU-Operationen erfordert: den alten Wert abrufen, einen Wert hinzufügen und dann den Wert speichern. Warum ist Atomizität wichtig? Wenn bei der Inkrementierungsoperation eine Race-Bedingung vorliegt, kann sich die gemeinsam genutzte Ressource (dh der gemeinsam genutzte Wert) jederzeit plötzlich ändern. Darüber hinaus sind Operationen mit 64-Bit-Strukturen, zum Beispiel longund double, nicht atomar. Weitere Details finden Sie hier: Stellen Sie die Atomizität beim Lesen und Schreiben von 64-Bit-Werten sicher . Probleme im Zusammenhang mit der Atomizität können in diesem Beispiel gesehen werden:

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());
    }
}
Die Sonderklasse AtomicIntegerwird uns immer 30.000 geben, aber das valuewird sich von Zeit zu Zeit ändern. Es gibt einen kurzen Überblick zu diesem Thema: Einführung in atomare Variablen in Java . Der „Compare-and-Swap“-Algorithmus ist das Herzstück atomarer Klassen. Mehr dazu können Sie hier im Vergleich lockfreier Algorithmen – CAS und FAA am Beispiel von JDK 7 und 8 oder im Compare-and-Swap- Artikel auf Wikipedia lesen. Besser zusammen: Java und die Thread-Klasse.  Teil III – Interaktion – 9

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

Passiert – vorher

Es gibt ein interessantes und mysteriöses Konzept namens „passiert vorher“. Im Rahmen Ihres Thread-Studiums sollten Sie darüber lesen. Die Passiert-Bevor-Beziehung zeigt die Reihenfolge, in der Aktionen zwischen Threads angezeigt werden. Es gibt viele Interpretationen und Kommentare. Hier ist eine der aktuellsten Präsentationen zu diesem Thema: Java „Happens-Before“ Relationships .

Zusammenfassung

In diesem Test haben wir einige Besonderheiten der Thread-Interaktion untersucht. Wir besprachen möglicherweise auftretende Probleme sowie Möglichkeiten, diese zu erkennen und zu beseitigen. Liste zusätzlicher Materialien zum Thema: Besser zusammen: Java und die Thread-Klasse. Teil I – Ausführungsthreads Gemeinsam besser: Java und die Thread-Klasse. Teil II – Synchronisierung Gemeinsam besser: Java und die Thread-Klasse. Teil IV – Callable, Future und Freunde Gemeinsam besser: Java und die Thread-Klasse. Teil V – Executor, ThreadPool, Fork/Join Gemeinsam besser: Java und die Thread-Klasse. Teil VI – Feuer los!
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION