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.
Wenn ein JVisualVM-Plugin installiert ist (über Extras -> Plugins), können wir sehen, wo der Deadlock aufgetreten ist:
Laut 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 .
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
Diese Inspektion wurde zu IntelliJ IDEA als Teil des Problems IDEA-61117 hinzugefügt, das bereits 2010 in den Versionshinweisen aufgeführt war .

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?
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
: 
"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-1
startet die Ausführung und führt die Friend#bow
Methode aus. Es ist mit dem synchronized
Schlü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 Friend
Objekt. Möchte nun Thread-1
die Methode auf dem anderen ausführen Friend
und 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-1
wartet 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-1
zuerst 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. 
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:
https://www.logicbig.com/
Thread.sleep()
auf Thread.wait()
die Last gleichmäßig verteilen können. 
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 newValue
Zuweisen eines Werts ein Fehler aufgetreten. newValue
ist zu groß. Aufgrund der Race-Bedingung gelang es einem der Threads, die Variablen value
zwischen 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,volatile
ist 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 flag
Feld nicht sehen. Um dies für das flag
Feld 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, dievolatile
Das 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 volatile
Schlü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 volatile
es 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: 
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 Beispiellong
und 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 AtomicInteger
wird uns immer 30.000 geben, aber das value
wird 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. 
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:- Doppelt überprüfte Verriegelung
- Häufig gestellte Fragen zu JSR 133 (Java-Speichermodell).
- IQ 35: Wie kann ein Deadlock verhindert werden?
- Parallelitätskonzepte in Java von Douglas Hawkins (2017)
GO TO FULL VERSION