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.
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.
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.
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
: Wenn 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 <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Thread 1 wartet auf eine Sperre von Thread 0. Warum passiert das? Thread-1
startet 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-1
möchte der Thread eine Methode auf einem anderen ausführen Friend
und 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-1
wartet Thread-0
und 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. Laut 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:https://www.logicbig.com/
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 newValue
etwas schiefgegangen und newValue
es gab noch mehr. Einige der Threads im Rennzustand haben es geschafft, value
zwischen 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 hervorzuhebenvolatile
. 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, flag
mü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 volatile
mit, 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 volatile
wird 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 volatile
hier 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: Diese 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 Beispiellong
und 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 Integer
zeigt uns immer 30000 an, aber value
das 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 “.
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: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:- Noch einmal zum doppelt überprüften Verschluss
- JSR 133 (Java Memory Model) FAQ (Übersetzung)
- Fortgeschrittenes Java – Parallelität (Yuri Tkach)
- Parallelitätskonzepte in Java von Douglas Hawkins (2017)
GO TO FULL VERSION