En kort oversigt over detaljerne i, hvordan tråde interagerer. Tidligere har vi set på, hvordan tråde er synkroniseret med hinanden. Denne gang vil vi dykke ned i de problemer, der kan opstå, når tråde interagerer, og vi vil tale om, hvordan man undgår dem. Vi vil også give nogle nyttige links til mere dybdegående undersøgelse.
Med et JVisualVM plugin installeret (via Værktøjer -> Plugins), kan vi se, hvor dødlåsen opstod:
Ifølge JVisualVM ser vi perioder med søvn og en parkperiode (det er, når en tråd forsøger at erhverve en lås - den går ind i parktilstanden, som vi diskuterede tidligere, da vi talte om trådsynkronisering ) . Du kan se et eksempel på livelock her: Java - Thread Livelock .
Du kan se et super eksempel her: Java - Thread Starvation and Fairness . Dette eksempel viser, hvad der sker med tråde under sult, og hvordan en lille ændring fra
Denne inspektion blev føjet til IntelliJ IDEA som en del af udgave IDEA-61117 , som blev opført i Release Notes tilbage i 2010.

Introduktion
Så vi ved, at Java har tråde. Det kan du læse om i anmeldelsen med titlen Better together: Java and the Thread class. Del I — Udførelsestråde . Og vi undersøgte det faktum, at tråde kan synkroniseres med hinanden i anmeldelsen med titlen Better together: Java and the Thread class. Del II — Synkronisering . Det er tid til at tale om, hvordan tråde interagerer med hinanden. Hvordan deler de delte ressourcer? Hvilke problemer kan der opstå her?
dødvande
Det mest skræmmende problem af alle er dødvande. Deadlock er, når to eller flere tråde evigt venter på den anden. Vi tager et eksempel fra Oracle-websiden, der beskriver dødvande :
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();
}
}
Deadlock forekommer muligvis ikke her første gang, men hvis dit program hænger, så er det tid til at køre 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
Tråd 1 venter på låsen fra tråd 0. Hvorfor sker det? Thread-1
begynder at køre og udfører Friend#bow
metoden. Det er markeret med nøgleordet synchronized
, hvilket betyder, at vi anskaffer monitoren til this
(det aktuelle objekt). Metodens input var en reference til det andet Friend
objekt. Nu Thread-1
ønsker at udføre metoden på den anden Friend
, og skal erhverve sin lås for at gøre det. Men hvis den anden tråd (i dette tilfælde Thread-0
) formåede at komme ind i bow()
metoden, så er låsen allerede blevet erhvervet og Thread-1
venter påThread-0
, og omvendt. Dette dødvande er uløseligt, og vi kalder det dødvande. Som et dødsgreb, der ikke kan slippes, er dødvande gensidig blokering, der ikke kan brydes. For en anden forklaring på deadlock, kan du se denne video: Deadlock og Livelock Explained .
Livelock
Hvis der er dødvande, er der så også livelock? Ja, det er der :) Livelock opstår, når tråde udadtil ser ud til at være i live, men de er ude af stand til at gøre noget, fordi de(n) betingelser, der kræves for, at de kan fortsætte deres arbejde, ikke kan opfyldes. Dybest set ligner livelock deadlock, men trådene "hænger" ikke og venter på en skærm. I stedet gør de for altid noget. For eksempel:
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();
}
}
Succesen med denne kode afhænger af den rækkefølge, som Java-trådplanlæggeren starter trådene i. Hvis Thead-1
starter først, så får vi 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
...
Som du kan se fra eksemplet, forsøger begge tråde at erhverve begge låse på skift, men de mislykkes. Men de er ikke i dødvande. Udadtil er alt godt, og de gør deres arbejde. 
Sult
Ud over deadlock og livelock er der et andet problem, der kan ske under multithreading: sult. Dette fænomen adskiller sig fra de tidligere former for blokering ved, at trådene ikke er blokeret - de har simpelthen ikke tilstrækkelige ressourcer. Som et resultat, mens nogle tråde tager al udførelsestiden, er andre ikke i stand til at køre:
https://www.logicbig.com/
Thread.sleep()
til Thread.wait()
lader dig fordele belastningen jævnt. 
Race forhold
I multithreading er der sådan noget som en "race condition". Dette fænomen opstår, når tråde deler en ressource, men koden er skrevet på en måde, så den ikke sikrer korrekt deling. Tag et kig på et eksempel:
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();
}
}
Denne kode genererer muligvis ikke en fejl første gang. Når det sker, kan det se sådan ud:
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)
Som du kan se, gik noget galt, mens newValue
der blev tildelt en værdi. newValue
er for stor. På grund af racetilstanden lykkedes det en af trådene at ændre variablerne value
mellem de to udsagn. Det viser sig, at der er et løb mellem trådene. Tænk nu på, hvor vigtigt det er ikke at lave lignende fejl med pengetransaktioner... Eksempler og diagrammer kan også ses her: Kode til at simulere racetilstand i Java-tråd .
Flygtig
Når vi taler om interaktionen mellem tråde,volatile
er nøgleordet værd at nævne. Lad os se på et simpelt eksempel:
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;
}
}
Mest interessant er det, at dette med stor sandsynlighed ikke virker. Den nye tråd vil ikke se ændringen i flag
feltet. For at rette dette for flag
feltet skal vi bruge søgeordet volatile
. Hvordan og hvorfor? Processoren udfører alle handlingerne. Men resultaterne af beregninger skal gemmes et sted. Til dette er der hovedhukommelse og der er processorens cache. En processors caches er som en lille del hukommelse, der bruges til at få adgang til data hurtigere, end når man får adgang til hovedhukommelsen. Men alt har en ulempe: dataene i cachen er muligvis ikke opdaterede (som i eksemplet ovenfor, når værdien af flagfeltet ikke blev opdateret). Såvolatile
nøgleord fortæller JVM, at vi ikke ønsker at cache vores variabel. Dette gør det muligt at se det opdaterede resultat på alle tråde. Dette er en meget forenklet forklaring. Hvad angår søgeordet volatile
, anbefaler jeg stærkt, at du læser denne artikel . For mere information råder jeg dig også til at læse Java Memory Model og Java Volatile Keyword . Derudover er det vigtigt at huske, at det volatile
handler om synlighed og ikke om atomiciteten af ændringer. Ser vi på koden i afsnittet "Racebetingelser", vil vi se et værktøjstip i IntelliJ IDEA: 
Atomicitet
Atomoperationer er operationer, der ikke kan opdeles. For eksempel skal operationen med at tildele en værdi til en variabel være atomisk. Desværre er inkrementoperationen ikke atomisk, fordi inkrement kræver så mange som tre CPU-operationer: Hent den gamle værdi, føj en til den, og gem derefter værdien. Hvorfor er atomicitet vigtig? Med inkrementoperationen, hvis der er en race-tilstand, kan den delte ressource (dvs. den delte værdi) pludselig ændre sig til enhver tid. Derudover er operationer, der involverer 64-bit strukturer, for eksempellong
og double
, ikke atomare. Flere detaljer kan læses her: Sørg for atomicitet ved læsning og skrivning af 64-bit værdier . Problemer relateret til atomicitet kan ses i dette eksempel:
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());
}
}
Specialklassen AtomicInteger
vil altid give os 30.000, men det value
vil ændre sig fra tid til anden. Der er en kort oversigt over dette emne: Introduktion til atomvariabler i Java . "Sammenlign-og-byt"-algoritmen ligger i hjertet af atomklasser. Du kan læse mere om det her i Sammenligning af låsefri algoritmer - CAS og FAA på eksemplet med JDK 7 og 8 eller i Sammenlign-og-byt- artiklen på Wikipedia. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Sker-før
Der er et interessant og mystisk koncept, der hedder "skeder før". Som en del af din undersøgelse af tråde, bør du læse om det. Sker-før-forholdet viser rækkefølgen, hvori handlinger mellem tråde vil blive set. Der er mange fortolkninger og kommentarer. Her er en af de seneste præsentationer om dette emne: Java "Happens-Before"-relationer .Resumé
I denne anmeldelse har vi undersøgt nogle af detaljerne i, hvordan tråde interagerer. Vi diskuterede problemer, der kan opstå, samt måder at identificere og eliminere dem på. Liste over yderligere materialer om emnet:- Dobbelttjekket låsning
- JSR 133 (Java Memory Model) FAQ
- IQ 35: Hvordan forhindrer man et dødvande?
- Concurrency Concepts in Java af Douglas Hawkins (2017)
GO TO FULL VERSION