CodeGym /Java Blog /Willekeurig /Samen beter: Java en de klasse Thread. Deel III — Interac...
John Squirrels
Niveau 41
San Francisco

Samen beter: Java en de klasse Thread. Deel III — Interactie

Gepubliceerd in de groep Willekeurig
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. Samen beter: Java en de klasse Thread.  Deel III — Interactie - 1

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? Samen beter: Java en de klasse Thread.  Deel III — Interactie - 2

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: Samen beter: Java en de klasse Thread.  Deel III — Interactie - 3Met een JVisualVM-plug-in geïnstalleerd (via Tools -> Plug-ins), kunnen we zien waar de deadlock is opgetreden:

"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-1begint te lopen en voert de Friend#bowmethode uit. Het is gemarkeerd met het synchronizedtrefwoord, wat betekent dat we de monitor aanschaffen voor this(het huidige object). De invoer van de methode was een verwijzing naar het andere Friendobject. Wil nu Thread-1de methode aan de andere kant uitvoeren Frienden 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-1wacht 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-1eerst 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. Samen beter: Java en de klasse Thread.  Deel III — Interactie - 4Volgens 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 .

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: Samen beter: Java en de klasse Thread.  Deel III — Interactie - 5

https://www.logicbig.com/

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 Thread.sleep()naar Thread.wait()de lading gelijkmatig kunt verdelen. Samen beter: Java en de klasse Thread.  Deel III — Interactie - 6

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 newValuehet toewijzen van een waarde. newValueis te groot. Vanwege de raceconditie slaagde een van de threads erin de variabelen valuetussen 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, het volatilesleutelwoord 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 flagveld 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 devolatiletrefwoord 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 volatilezoekwoord 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 volatilehet 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: Samen beter: Java en de klasse Thread.  Deel III — Interactie - 7Deze inspectie is toegevoegd aan IntelliJ IDEA als onderdeel van nummer IDEA-61117 , dat in 2010 werd vermeld in de release-opmerkingen .

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, bijvoorbeeld longen 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 AtomicIntegerklasse geeft ons altijd 30.000, maar dit valuezal 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.Samen beter: Java en de klasse Thread.  Deel III — Interactie - 9

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: Samen beter: Java en de klasse Thread. Deel I — Uitvoeringsthreads Beter samen: Java en de klasse Thread. Deel II — Synchronisatie Samen beter: Java en de klasse Thread. Deel IV — Callable, Future en vrienden Samen beter: Java en de Thread-klasse. Deel V — Uitvoerder, ThreadPool, Fork/Join Beter samen: Java en de Thread-klasse. Deel VI — Vuur weg!
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION