Rövid áttekintés a szálak kölcsönhatásának részleteiről. Korábban megvizsgáltuk, hogy a szálak hogyan szinkronizálódnak egymással. Ezúttal belevetjük magunkat a szálak kölcsönhatása során felmerülő problémákba, és beszélünk arról, hogyan lehet ezeket elkerülni. Néhány hasznos linket is biztosítunk az alaposabb tanulmányozáshoz.
Egy JVisualVM beépülő modul telepítésével (az Eszközök -> Beépülő modulok segítségével) láthatjuk, hol történt a holtpont:
A JVisualVM szerint alvási és parkolási periódusokat látunk (ez az, amikor egy szál megpróbál zárat szerezni – park állapotba kerül, amint arról korábban beszéltünk, amikor a szálak szinkronizálásáról beszéltünk ) . Itt láthat egy példát a livelockra: Java - Thread Livelock .
Itt láthatsz egy szuper példát: Java - Thread Starvation and Fairness . Ez a példa bemutatja, hogy mi történik a szálakkal az éhezés során, és hogyan lehet egy kis változtatással
kiadás részeként adták hozzá az IntelliJ IDEA-hoz , amely 2010-ben szerepelt a kiadási megjegyzésekben .

Bevezetés
Tehát tudjuk, hogy a Java-nak vannak szálai. Erről a Jobb együtt: Java és a szál osztály című ismertetőben olvashat . I. rész – A végrehajtás szálai . Azt pedig megvizsgáltuk, hogy a szálak szinkronizálhatók egymással a Jobb együtt: Java és a szál osztály című áttekintésben . II. rész – Szinkronizálás . Itt az ideje, hogy beszéljünk arról, hogyan hatnak egymásra a szálak. Hogyan osztják meg a megosztott erőforrásokat? Milyen problémák merülhetnek fel itt?
Holtpont
A legfélelmetesebb probléma a holtpont. A holtpont az, amikor két vagy több szál örökké a másikra vár. Vegyünk egy példát az Oracle weboldaláról, amely leírja a holtpontot :
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();
}
}
Előfordulhat, hogy a holtpont nem itt történik először, de ha a program lefagy, akkor ideje futni 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
Az 1. szál a 0. szál zárolását várja. Miért történik ez? Thread-1
elindul, és végrehajtja a Friend#bow
metódust. A kulcsszóval van jelölve synchronized
, ami azt jelenti, hogy (az aktuális objektumhoz) vásároljuk meg a monitort this
. A metódus bemenete a másik objektumra való hivatkozás volt Friend
. Most Thread-1
szeretné végrehajtani a metódust a másikon Friend
, és ehhez be kell szereznie a zárolását. De ha a másik szálnak (ebben az esetben Thread-0
) sikerült belépnie a bow()
metódusba, akkor a zár már megvan és Thread-1
várThread-0
, és fordítva. Ez a zsákutca feloldhatatlan, és ezt nevezzük holtpontnak. Mint egy halálmarkolat, amelyet nem lehet elengedni, a holtpont kölcsönös blokkolás, amelyet nem lehet feltörni. A holtpont másik magyarázatához ezt a videót tekintheti meg: Deadlock and Livelock Explained .
Livelock
Ha van holtpont, akkor van livelock is? Igen, van :) A Livelock akkor történik, amikor a szálak kifelé élnek, de nem tudnak mit tenni, mert nem teljesülnek a munkájuk folytatásához szükséges feltétel(ek). Alapvetően a livelock hasonló a holtponthoz, de a szálak nem "lógnak" monitorra várva. Ehelyett örökké csinálnak valamit. Például:
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();
}
}
A kód sikere attól függ, hogy a Java szálütemező milyen sorrendben indítja el a szálakat. Ha Thead-1
először indul, akkor livelockot kapunk:
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
...
Amint a példából látható, mindkét szál megpróbálja felváltva megszerezni mindkét zárat, de nem sikerül. De nincsenek holtponton. Külsőleg minden rendben van és teszik a dolgukat. 
Éhezés
A holtponton és a livelockon kívül egy másik probléma is előfordulhat a többszálú feldolgozás során: az éhezés. Ez a jelenség abban különbözik a blokkolás korábbi formáitól, hogy a szálak nincsenek blokkolva – egyszerűen nem rendelkeznek elegendő erőforrással. Ennek eredményeként, míg egyes szálak a teljes végrehajtási időt igénybe veszik, mások nem tudnak futni:
https://www.logicbig.com/
Thread.sleep()
egyenletesen Thread.wait()
elosztani a terhelést. 
A verseny feltételei
A multithreadingben létezik olyan, hogy "versenyfeltétel". Ez a jelenség akkor fordul elő, ha a szálak megosztanak egy erőforrást, de a kód úgy van megírva, hogy az nem biztosítja a helyes megosztást. Vessen egy pillantást egy példára:
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();
}
}
Előfordulhat, hogy ez a kód nem generál hibát az első alkalommal. Amikor megtörténik, így nézhet ki:
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)
Amint látja, valami hiba történt newValue
az érték hozzárendelése közben. newValue
túl nagy. A versenyfeltételek miatt az egyik szálnak sikerült megváltoztatnia a változókat value
a két utasítás között. Kiderül, hogy verseny van a szálak között. Most gondoljon bele, mennyire fontos, hogy ne kövessünk el hasonló hibákat a pénzügyleteknél... Példák és diagramok is láthatók itt: Code to simulate race condition in Java thread .
Illó
A szálak interakciójáról avolatile
kulcsszót érdemes megemlíteni. Nézzünk egy egyszerű példát:
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;
}
}
A legérdekesebb, hogy ez nagy valószínűséggel nem fog működni. Az új szál nem fogja látni a változást a mezőben flag
. Ennek kijavításához a flag
mezőben a kulcsszót kell használnunk volatile
. Hogyan és miért? A processzor végrehajtja az összes műveletet. De a számítások eredményeit valahol el kell tárolni. Ehhez van fő memória és van a processzor gyorsítótára. A processzor gyorsítótárai olyanok, mint egy kis memóriadarab, amellyel gyorsabban lehet hozzáférni az adatokhoz, mint a főmemória elérésekor. De mindennek van egy hátránya: előfordulhat, hogy a gyorsítótárban lévő adatok nem naprakészek (mint a fenti példában, amikor a jelzőmező értéke nem frissült). Így avolatile
kulcsszó azt mondja a JVM-nek, hogy nem akarjuk a változónkat gyorsítótárba helyezni. Ez lehetővé teszi, hogy a naprakész eredmény minden szálon látható legyen. Ez egy nagyon leegyszerűsített magyarázat. Ami a volatile
kulcsszót illeti, nagyon ajánlom, hogy olvassa el ezt a cikket . További információért azt is tanácsolom, hogy olvassa el a Java memóriamodell és a Java volatile kulcsszó című részt . Ezenkívül fontos megjegyezni, hogy ez volatile
a láthatóságról szól, és nem a változások atomitásáról. A „Versenyfeltételek” részben található kódot tekintve az IntelliJ IDEA eszköztippje látható: Ezt az ellenőrzést az IDEA-61117
Atomos állapot
Az atomi műveletek olyan műveletek, amelyek nem oszthatók fel. Például egy változóhoz való érték hozzárendelésének atomi jellegűnek kell lennie. Sajnos a növekmény művelet nem atomi, mert a növekményhez akár három CPU-művelet is szükséges: a régi érték előállítása, egy hozzáadás, majd az érték mentése. Miért fontos az atomitás? Az inkrementális műveletnél, ha versenyfeltétel van, akkor a megosztott erőforrás (azaz a megosztott érték) bármikor hirtelen megváltozhat. Ezenkívül a 64 bites struktúrákat érintő műveletek, példáullong
és double
, nem atomi. További részletek itt olvashatók: Biztosítsa az atomitást 64 bites értékek olvasása és írása során . Az atomitással kapcsolatos problémák ebben a példában láthatók:
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());
}
}
A speciális AtomicInteger
osztály mindig ad nekünk 30 ezret, de ez value
időről időre változik. Erről a témáról van egy rövid áttekintés: Bevezetés a Java Atomic Variables-be . Az "összehasonlítás és csere" algoritmus az atomosztályok középpontjában áll. Erről itt olvashat bővebben a Zárolásmentes algoritmusok összehasonlítása - CAS és FAA a JDK 7 és 8 példáján vagy a Wikipédia Összehasonlítás és csere című cikkében. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Előtte történik
Van egy érdekes és titokzatos koncepció, az úgynevezett „korábban történik”. A szálak tanulmányozása részeként érdemes olvasni róla. Az előtte történik kapcsolat azt a sorrendet mutatja, amelyben a szálak közötti műveletek megjelennek. Sokféle értelmezés és kommentár létezik. Íme az egyik legfrissebb előadás ebben a témában: Java "Happens-Before" Relationships .Összegzés
Ebben az áttekintésben megvizsgáltuk a szálak interakciójának néhány sajátosságát. Megbeszéltük a felmerülő problémákat, valamint azok azonosításának és megszüntetésének módjait. További anyagok listája a témában:- Duplán ellenőrzött zár
- JSR 133 (Java memóriamodell) GYIK
- IQ 35: Hogyan lehet megelőzni a holtpontot?
- Egyidejűségi fogalmak a Java nyelven, Douglas Hawkins (2017)
GO TO FULL VERSION