CodeGym /Java blogg /Slumpmässig /Bättre tillsammans: Java och trådklassen. Del III – Inter...
John Squirrels
Nivå
San Francisco

Bättre tillsammans: Java och trådklassen. Del III – Interaktion

Publicerad i gruppen
En kort översikt över detaljerna i hur trådar interagerar. Tidigare har vi tittat på hur trådar synkroniseras med varandra. Den här gången kommer vi att dyka in i de problem som kan uppstå när trådar interagerar, och vi kommer att prata om hur man undviker dem. Vi kommer också att tillhandahålla några användbara länkar för mer djupgående studier. Bättre tillsammans: Java och trådklassen.  Del III — Interaktion - 1

Introduktion

Så vi vet att Java har trådar. Det kan du läsa om i recensionen Better together: Java and the Thread class. Del I — Avrättningstrådar . Och vi utforskade det faktum att trådar kan synkroniseras med varandra i recensionen med titeln Better together: Java and the Thread class. Del II — Synkronisering . Det är dags att prata om hur trådar interagerar med varandra. Hur delar de delade resurser? Vilka problem kan uppstå här? Bättre tillsammans: Java och trådklassen.  Del III — Interaktion - 2

Dödläge

Det läskigaste problemet av allt är dödläget. Dödläge är när två eller flera trådar evigt väntar på den andra. Vi tar ett exempel från Oracles webbsida som beskriver dödläge :

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 kanske inte inträffar här första gången, men om ditt program hänger sig, är det dags att köra jvisualvm: Bättre tillsammans: Java och trådklassen.  Del III — Interaktion - 3Med en JVisualVM-plugin installerad (via Verktyg -> Plugins), kan vi se var dödläget inträffade:

"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 väntar på låset från tråd 0. Varför händer det? Thread-1börjar köra och kör Friend#bowmetoden. Den är markerad med synchronizednyckelordet, vilket betyder att vi skaffar monitorn för ( thisdet aktuella objektet). Metodens input var en referens till det andra Friendobjektet. Vill nu Thread-1köra metoden på den andra , Friendoch måste skaffa dess lås för att göra det. Men om den andra tråden (i det här fallet Thread-0) lyckades komma in i bow()metoden, har låset redan förvärvats och Thread-1väntar påThread-0, och vice versa. Detta återvändsgränd är olösligt, och vi kallar det dödläge. Som ett dödsgrepp som inte kan släppas, är dödläge en ömsesidig blockering som inte kan brytas. För en annan förklaring av dödläge kan du titta på den här videon: Deadlock och Livelock Explained .

Livelock

Om det finns dödläge, finns det även livelock? Ja, det finns :) Livelock händer när trådar utåt verkar vara levande, men de kan inte göra någonting, eftersom villkoren/villkoren som krävs för att de ska fortsätta sitt arbete inte kan uppfyllas. I grund och botten liknar livelock deadlock, men trådarna "hänger" inte i väntan på en bildskärm. Istället gör de något för alltid. Till exempel:

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();
    }
}
Framgången för denna kod beror på i vilken ordning Java-trådschemaläggaren startar trådarna. Om Thead-1startar först 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 från exemplet försöker båda trådarna att skaffa båda låsen i tur och ordning, men de misslyckas. Men de är inte i dödläge. Utåt sett är allt bra och de gör sitt jobb. Bättre tillsammans: Java och trådklassen.  Del III — Interaktion - 4Enligt JVisualVM ser vi perioder av sömn och en period av park (detta är när en tråd försöker få ett lås — den går in i parktillståndet, som vi diskuterade tidigare när vi pratade om trådsynkronisering ) . Du kan se ett exempel på livelock här: Java - Thread Livelock .

Svält

Förutom deadlock och livelock finns det ett annat problem som kan hända under multithreading: svält. Detta fenomen skiljer sig från de tidigare formerna av blockering genom att trådarna inte är blockerade — de har helt enkelt inte tillräckliga resurser. Som ett resultat, medan vissa trådar tar all körningstid, kan andra inte köras: Bättre tillsammans: Java och trådklassen.  Del III — Interaktion - 5

https://www.logicbig.com/

Du kan se ett superexempel här: Java - Thread Starvation and Fairness . Det här exemplet visar vad som händer med trådar under svält och hur en liten förändring från Thread.sleep()till Thread.wait()låter dig fördela belastningen jämnt. Bättre tillsammans: Java och trådklassen.  Del III — Interaktion - 6

Tävlingsförhållanden

I multithreading finns det något som heter "race condition". Detta fenomen inträffar när trådar delar en resurs, men koden är skriven på ett sätt så att den inte säkerställer korrekt delning. Ta en titt på ett exempel:

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();
    }
}
Den här koden kanske inte genererar ett fel första gången. När det gör det kan det se ut så här:

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 gick något fel när newValueett värde tilldelades. newValueär för stor. På grund av rastillståndet lyckades en av trådarna ändra variabeln valuemellan de två påståendena. Det visar sig att det är ett lopp mellan trådarna. Tänk nu på hur viktigt det är att inte göra liknande misstag med monetära transaktioner... Exempel och diagram kan också ses här: Kod för att simulera rastillstånd i Java-tråd .

Flyktig

På tal om interaktionen mellan trådar volatileär nyckelordet värt att nämna. Låt oss titta på ett enkelt exempel:

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;
    }
}
Det mest intressanta är att detta med stor sannolikhet inte kommer att fungera. Den nya tråden kommer inte att se förändringen i fältet flag. För att fixa detta för flagfältet måste vi använda volatilenyckelordet. Hur och varför? Processorn utför alla åtgärder. Men resultaten av beräkningar måste lagras någonstans. För detta finns huvudminne och det finns processorns cache. En processors cacheminne är som en liten bit minne som används för att komma åt data snabbare än när man kommer åt huvudminnet. Men allt har en baksida: data i cachen kanske inte är uppdaterade (som i exemplet ovan, när värdet på flaggfältet inte uppdaterades). Alltsåvolatilenyckelordet talar om för JVM att vi inte vill cachelagra vår variabel. Detta gör att det uppdaterade resultatet kan ses på alla trådar. Detta är en mycket förenklad förklaring. När det gäller volatilenyckelordet rekommenderar jag starkt att du läser den här artikeln . För mer information råder jag dig också att läsa Java Memory Model och Java Volatile Keyword . Dessutom är det viktigt att komma ihåg att det volatilehandlar om synlighet, och inte om atomiciteten av förändringar. Om vi ​​tittar på koden i avsnittet "Race conditions" kommer vi att se ett verktygstips i IntelliJ IDEA: Bättre tillsammans: Java och trådklassen.  Del III — Interaktion - 7Denna inspektion lades till IntelliJ IDEA som en del av nummer IDEA-61117, som listades i Release Notes redan 2010.

Atomicitet

Atomoperationer är operationer som inte kan delas upp. Till exempel måste operationen att tilldela ett värde till en variabel vara atomär. Tyvärr är inkrementoperationen inte atomär, eftersom inkrement kräver så många som tre CPU-operationer: hämta det gamla värdet, lägg till en till det och spara sedan värdet. Varför är atomicitet viktigt? Med inkrementoperationen, om det finns ett tävlingstillstånd, kan den delade resursen (dvs. det delade värdet) plötsligt ändras när som helst. Dessutom är operationer som involverar 64-bitars strukturer, till exempel longoch double, inte atomära. Mer detaljer kan läsas här: Säkerställ atomicitet när du läser och skriver 64-bitars värden . Problem relaterade till atomicitet kan ses i detta exempel:

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 AtomicIntegerkommer alltid att ge oss 30 000, men de valuekommer att ändras från tid till annan. Det finns en kort översikt över detta ämne: Introduktion till atomvariabler i Java . "Jämför-och-byt"-algoritmen ligger i hjärtat av atomklasser. Du kan läsa mer om det här i Jämförelse av låsfria algoritmer - CAS och FAA på exemplet JDK 7 och 8 eller i Jämför-och-byt- artikeln på Wikipedia. Bättre tillsammans: Java och trådklassen.  Del III — Interaktion - 9

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

Händer-före

Det finns ett intressant och mystiskt koncept som heter "händer innan". Som en del av din studie av trådar bör du läsa om det. Händer-före-relationen visar i vilken ordning åtgärder mellan trådar kommer att ses. Det finns många tolkningar och kommentarer. Här är en av de senaste presentationerna om detta ämne: Java "Happens-Before"-relationer .

Sammanfattning

I den här recensionen har vi utforskat några detaljer om hur trådar interagerar. Vi diskuterade problem som kan uppstå, samt sätt att identifiera och eliminera dem. Lista över ytterligare material om ämnet: Bättre tillsammans: Java och trådklassen. Del I — Trådar av utförande Bättre tillsammans: Java och klassen Thread. Del II — Synkronisering Bättre tillsammans: Java och klassen Thread. Del IV — Callable, Future och friends Bättre tillsammans: Java och Thread-klassen. Del V — Executor, ThreadPool, Fork/Join Better tillsammans: Java och Thread-klassen. Del VI — Skjut loss!
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION