Een kort overzicht van de bijzonderheden van hoe threads op elkaar inwerken. Eerder hebben we gekeken hoe threads met elkaar worden gesynchroniseerd. Deze keer gaan we dieper in op de problemen die zich kunnen voordoen wanneer threads op elkaar inwerken, en bespreken we hoe we deze kunnen vermijden. We zullen ook enkele nuttige links geven voor meer diepgaande studie.
Met een JVisualVM-plug-in geïnstalleerd (via Tools -> Plug-ins), kunnen we zien waar de deadlock is opgetreden:
Volgens JVisualVM zien we perioden van slaap en een periode van parkeren (dit is wanneer een thread een slot probeert te krijgen - hij komt in de parkstatus, zoals we eerder bespraken toen we het hadden over threadsynchronisatie ) . U kunt hier een voorbeeld van livelock zien: Java - Thread Livelock .
Je kunt hier een supervoorbeeld zien: Java - Thread Starvation and Fairness . Dit voorbeeld laat zien wat er gebeurt met draden tijdens uithongering en hoe je met een kleine verandering van
Deze inspectie is toegevoegd aan IntelliJ IDEA als onderdeel van nummer IDEA-61117 , dat in 2010 werd vermeld in de release-opmerkingen .

Invoering
We weten dus dat Java threads heeft. Dat lees je in de recensie Better together: Java and the Thread class. Deel I — Draden van executie . En we onderzochten het feit dat threads met elkaar kunnen synchroniseren in de recensie getiteld Better Together: Java and the Thread class. Deel II — Synchronisatie . Het is tijd om te praten over hoe threads met elkaar omgaan. Hoe delen ze gedeelde bronnen? Welke problemen kunnen zich hier voordoen?
Impasse
Het engste probleem van allemaal is een impasse. Deadlock is wanneer twee of meer threads eeuwig op de ander wachten. We nemen een voorbeeld van de Oracle-webpagina die deadlock beschrijft :
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();
}
}
Het kan zijn dat hier de eerste keer geen deadlock optreedt, maar als uw programma vastloopt, is het tijd om uit te voeren 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
Draad 1 wacht op de vergrendeling van draad 0. Waarom gebeurt dat? Thread-1
begint te lopen en voert de Friend#bow
methode uit. Het is gemarkeerd met het synchronized
trefwoord, wat betekent dat we de monitor aanschaffen voor this
(het huidige object). De invoer van de methode was een verwijzing naar het andere Friend
object. Wil nu Thread-1
de methode aan de andere kant uitvoeren Friend
en moet daarvoor zijn vergrendeling verkrijgen. Maar als de andere thread (in dit geval Thread-0
) erin is geslaagd om de bow()
methode binnen te gaan, dan is het slot al verkregen en Thread-1
wacht het opThread-0
, en vice versa. Deze impasse is onoplosbaar en we noemen het een impasse. Net als een dodelijke greep die niet kan worden losgelaten, is een impasse een wederzijdse blokkering die niet kan worden doorbroken. Voor een andere uitleg van deadlock kun je deze video bekijken: Deadlock en Livelock Explained .
Livelock
Als er een impasse is, is er dan ook een livelock? Ja, dat is er :) Livelock vindt plaats wanneer draden uiterlijk lijken te leven, maar ze niets kunnen doen, omdat niet kan worden voldaan aan de voorwaarde(n) die nodig zijn om hun werk voort te zetten. In principe is livelock vergelijkbaar met deadlock, maar de threads "hangen" niet terwijl ze wachten op een monitor. In plaats daarvan zijn ze voor altijd iets aan het doen. Bijvoorbeeld:
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();
}
}
Het succes van deze code hangt af van de volgorde waarin de Java-threadplanner de threads start. Als Thead-1
eerst begint, krijgen we 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
...
Zoals je in het voorbeeld kunt zien, proberen beide threads beurtelings beide sloten te verkrijgen, maar dat mislukt. Maar ze zitten niet in een impasse. Uiterlijk is alles in orde en doen ze hun werk. 
Honger
Naast deadlock en livelock is er nog een ander probleem dat kan optreden tijdens multithreading: uithongering. Dit fenomeen verschilt van de eerdere vormen van blokkering doordat de threads niet worden geblokkeerd — ze hebben simpelweg niet voldoende middelen. Als gevolg hiervan, terwijl sommige threads alle uitvoeringstijd in beslag nemen, kunnen andere niet worden uitgevoerd:
https://www.logicbig.com/
Thread.sleep()
naar Thread.wait()
de lading gelijkmatig kunt verdelen. 
Race voorwaarden
Bij multithreading bestaat er zoiets als een "raceconditie". Dit fenomeen doet zich voor wanneer threads een bron delen, maar de code zo is geschreven dat deze niet correct kan worden gedeeld. Bekijk een voorbeeld:
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();
}
}
Deze code genereert mogelijk niet de eerste keer een fout. Wanneer dit het geval is, kan het er zo uitzien:
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)
Zoals u kunt zien, is er iets misgegaan bij newValue
het toewijzen van een waarde. newValue
is te groot. Vanwege de raceconditie slaagde een van de threads erin de variabelen value
tussen de twee statements te wijzigen. Het blijkt dat er een race tussen de draden is. Bedenk nu eens hoe belangrijk het is om soortgelijke fouten niet te maken met geldtransacties... Voorbeelden en diagrammen zijn ook hier te zien: Code om raceconditie te simuleren in Java-thread .
Vluchtig
Over de interactie van threads gesproken, hetvolatile
sleutelwoord is het vermelden waard. Laten we een eenvoudig voorbeeld bekijken:
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;
}
}
Het meest interessante is dat dit zeer waarschijnlijk niet werkt. De nieuwe thread zal de wijziging in het veld niet zien flag
. Om dit voor het flag
veld op te lossen, moeten we het trefwoord gebruiken volatile
. Hoe en waarom? De processor voert alle acties uit. Maar de resultaten van berekeningen moeten ergens worden opgeslagen. Hiervoor is er het hoofdgeheugen en is er de cache van de processor. De caches van een processor zijn als een klein stukje geheugen dat wordt gebruikt om sneller toegang te krijgen tot gegevens dan bij toegang tot het hoofdgeheugen. Maar alles heeft een keerzijde: de gegevens in de cache zijn mogelijk niet up-to-date (zoals in het voorbeeld hierboven, toen de waarde van het vlagveld niet werd bijgewerkt). Dus devolatile
trefwoord vertelt de JVM dat we onze variabele niet in de cache willen opslaan. Hierdoor is het actuele resultaat op alle threads te zien. Dit is een sterk vereenvoudigde uitleg. Wat het volatile
zoekwoord betreft, raad ik u ten zeerste aan dit artikel te lezen . Voor meer informatie raad ik je ook aan Java Memory Model en Java Volatile Keyword te lezen . Daarnaast is het belangrijk om te onthouden dat volatile
het om de zichtbaarheid gaat, en niet om de atomiciteit van veranderingen. Als we naar de code in het gedeelte "Racevoorwaarden" kijken, zien we een tooltip in IntelliJ IDEA: 
atomiciteit
Atomaire operaties zijn operaties die niet kunnen worden gedeeld. De bewerking van het toekennen van een waarde aan een variabele moet bijvoorbeeld atomair zijn. Helaas is de increment-bewerking niet atomair, omdat voor increment maar liefst drie CPU-bewerkingen nodig zijn: haal de oude waarde op, voeg er een toe en sla de waarde op. Waarom is atomiciteit belangrijk? Met de increment-bewerking kan, als er een raceconditie is, de gedeelde hulpbron (dwz de gedeelde waarde) op elk moment plotseling veranderen. Bovendien zijn bewerkingen met 64-bits structuren, bijvoorbeeldlong
en double
, niet atomair. Meer details zijn hier te lezen: Zorg voor atomiciteit bij het lezen en schrijven van 64-bits waarden . Problemen met betrekking tot atomiciteit zijn te zien in dit voorbeeld:
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());
}
}
De speciale AtomicInteger
klasse geeft ons altijd 30.000, maar dit value
zal van tijd tot tijd veranderen. Er is een kort overzicht van dit onderwerp: Inleiding tot atomaire variabelen in Java . Het "compare-and-swap"-algoritme vormt de kern van atomaire klassen. U kunt er hier meer over lezen in Vergelijking van vergrendelingsvrije algoritmen - CAS en FAA op het voorbeeld van JDK 7 en 8 of in het artikel Compare-and-swap op Wikipedia.
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Gebeurt eerder
Er is een interessant en mysterieus concept genaamd "gebeurt eerder". Als onderdeel van je studie van threads, zou je erover moeten lezen. De voor-gebeurtenis-relatie toont de volgorde waarin acties tussen threads worden gezien. Er zijn veel interpretaties en commentaren. Hier is een van de meest recente presentaties over dit onderwerp: Java "Happens-Before"-relaties .Samenvatting
In deze review hebben we enkele details onderzocht van hoe threads op elkaar inwerken. We bespraken problemen die zich kunnen voordoen, evenals manieren om ze te identificeren en op te lossen. Lijst met aanvullend materiaal over het onderwerp:- Dubbel gecontroleerde vergrendeling
- Veelgestelde vragen over JSR 133 (Java-geheugenmodel).
- IQ 35: Hoe voorkom je een impasse?
- Gelijktijdigheidsconcepten in Java door Douglas Hawkins (2017)
GO TO FULL VERSION