CodeGym/Java blog/Véletlen/Jobb együtt: Java és a Thread osztály. III. rész – Kölcsö...
John Squirrels
Szint
San Francisco

Jobb együtt: Java és a Thread osztály. III. rész – Kölcsönhatás

Megjelent a csoportban
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. Jobb együtt: Java és a Thread osztály.  III. rész – Kölcsönhatás – 1

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? Jobb együtt: Java és a Thread osztály.  III. rész – Kölcsönhatás – 2

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: Jobb együtt: Java és a Thread osztály.  III. rész – Kölcsönhatás – 3Egy 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:
"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-1elindul, és végrehajtja a Friend#bowmetó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-1szeretné 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-1vá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-1elő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. Jobb együtt: Java és a Thread osztály.  III. rész – Kölcsönhatás – 4A 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 .

É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: Jobb együtt: Java és a Thread osztály.  III. rész – Kölcsönhatás – 5

https://www.logicbig.com/

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 Thread.sleep()egyenletesen Thread.wait()elosztani a terhelést. Jobb együtt: Java és a Thread osztály.  III. rész – Kölcsönhatás – 6

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 newValueaz érték hozzárendelése közben. newValuetúl nagy. A versenyfeltételek miatt az egyik szálnak sikerült megváltoztatnia a változókat valuea 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 a volatilekulcsszó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 flagmező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 avolatilekulcsszó 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 volatilekulcsszó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 volatilea 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-61117Jobb együtt: Java és a Thread osztály.  III. rész – Kölcsönhatás – 7 kiadás részeként adták hozzá az IntelliJ IDEA-hoz , amely 2010-ben szerepelt a kiadási megjegyzésekben .

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ául longé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 AtomicIntegerosztály mindig ad nekünk 30 ezret, de ez valueidő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. Jobb együtt: Java és a Thread osztály.  III. rész – Kölcsönhatás – 9

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: Jobb együtt: Java és a Thread osztály. I. rész – A végrehajtás szálai Jobb együtt: Java és a Thread osztály. II. rész – Szinkronizálás Jobb együtt: Java és a Thread osztály. IV. rész – Hívható, jövő és barátok Jobb együtt: Java és a szál osztály. V. rész – Végrehajtó, ThreadPool, Fork/Join Better together: Java és a Thread osztály. VI. rész – Tüzet el!
Hozzászólások
  • Népszerű
  • Új
  • Régi
Hozzászólás írásához be kell jelentkeznie
Ennek az oldalnak még nincsenek megjegyzései