CodeGym /Java-blogg /Tilfeldig /Bedre sammen: Java og Thread-klassen. Del III – Samhandli...
John Squirrels
Nivå
San Francisco

Bedre sammen: Java og Thread-klassen. Del III – Samhandling

Publisert i gruppen
En kort oversikt over detaljene i hvordan tråder samhandler. Tidligere har vi sett på hvordan tråder er synkronisert med hverandre. Denne gangen skal vi dykke ned i problemene som kan oppstå når tråder samhandler, og vi skal snakke om hvordan vi kan unngå dem. Vi vil også gi noen nyttige lenker for mer dybdestudie. Bedre sammen: Java og Thread-klassen.  Del III – Interaksjon – 1

Introduksjon

Så vi vet at Java har tråder. Det kan du lese om i anmeldelsen Bedre sammen: Java og trådklassen. Del I – Tråder om henrettelse . Og vi utforsket det faktum at tråder kan synkroniseres med hverandre i anmeldelsen med tittelen Better together: Java and the Thread class. Del II – Synkronisering . Det er på tide å snakke om hvordan tråder samhandler med hverandre. Hvordan deler de delte ressurser? Hvilke problemer kan oppstå her? Bedre sammen: Java og Thread-klassen.  Del III – Interaksjon – 2

Dødlås

Det skumleste problemet av alle er dødlås. Deadlock er når to eller flere tråder evig venter på den andre. Vi tar et eksempel fra Oracle-nettsiden som beskriver deadlock :

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();
    }
}
Det kan hende at det ikke oppstår dødlås her første gang, men hvis programmet ditt henger, er det på tide å kjøre jvisualvm: Bedre sammen: Java og Thread-klassen.  Del III – Interaksjon – 3Med en JVisualVM-plugin installert (via Verktøy -> Plugins), kan vi se hvor dødlåsen oppsto:

"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 venter på låsen fra tråd 0. Hvorfor skjer det? Thread-1starter å kjøre og kjører Friend#bowmetoden. Det er merket med nøkkelordet synchronized, som betyr at vi anskaffer monitoren for this(det gjeldende objektet). Metodens input var en referanse til det andre Friendobjektet. Nå Thread-1ønsker å utføre metoden på den andre Friend, og må anskaffe låsen for å gjøre det. Men hvis den andre tråden (i dette tilfellet Thread-0) klarte å gå inn i bow()metoden, er låsen allerede ervervet og Thread-1venter påThread-0, og vice versa. Dette er blindgate er uløselig, og vi kaller det dødlås. Som et dødsgrep som ikke kan slippes, er dødlås gjensidig blokkering som ikke kan brytes. For en annen forklaring på deadlock, kan du se denne videoen: Deadlock og Livelock Explained .

Livelock

Hvis det er dødlås, er det også livelock? Ja, det er det :) Livelock skjer når tråder utad ser ut til å være i live, men de klarer ikke å gjøre noe, fordi betingelsen(e) som kreves for at de skal fortsette arbeidet ikke kan oppfylles. I utgangspunktet ligner livelock på deadlock, men trådene "henger" ikke mens de venter på en skjerm. I stedet gjør de noe for alltid. For eksempel:

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();
    }
}
Suksessen til denne koden avhenger av rekkefølgen som Java-trådplanleggeren starter trådene i. Hvis Thead-1starter 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 fra eksempelet prøver begge trådene å skaffe begge låsene etter tur, men de mislykkes. Men de er ikke i fastlåst tilstand. Utad er alt bra og de gjør jobben sin. Bedre sammen: Java og Thread-klassen.  Del III – Interaksjon – 4I følge JVisualVM ser vi perioder med søvn og en periode med park (dette er når en tråd prøver å skaffe seg en lås — den går inn i parktilstanden, som vi diskuterte tidligere da vi snakket om trådsynkronisering ) . Du kan se et eksempel på livelock her: Java - Thread Livelock .

Sult

I tillegg til deadlock og livelock, er det et annet problem som kan oppstå under multithreading: sult. Dette fenomenet skiller seg fra de tidligere blokkeringsformene ved at trådene ikke er blokkert - de har rett og slett ikke tilstrekkelige ressurser. Som et resultat, mens noen tråder tar all utførelsestiden, kan andre ikke kjøre: Bedre sammen: Java og Thread-klassen.  Del III – Interaksjon – 5

https://www.logicbig.com/

Du kan se et supereksempel her: Java - Thread Starvation and Fairness . Dette eksemplet viser hva som skjer med tråder under sult og hvordan en liten endring fra Thread.sleep()til Thread.wait()lar deg fordele belastningen jevnt. Bedre sammen: Java og Thread-klassen.  Del III – Interaksjon – 6

Løpsforhold

I multithreading er det noe som heter "race condition". Dette fenomenet skjer når tråder deler en ressurs, men koden er skrevet på en måte som ikke sikrer riktig deling. Ta en titt på et eksempel:

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();
    }
}
Denne koden genererer kanskje ikke en feil første gang. Når det gjør det, kan det se slik ut:

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, gikk noe galt mens newValueen verdi ble tildelt. newValueer for stor. På grunn av rasetilstanden klarte en av trådene å endre variablene valuemellom de to utsagnene. Det viser seg at det er et løp mellom trådene. Tenk nå på hvor viktig det er å ikke gjøre lignende feil med pengetransaksjoner... Eksempler og diagrammer kan også sees her: Kode for å simulere rasetilstand i Java-tråd .

Flyktige

Når vi snakker om samspillet mellom tråder, volatileer nøkkelordet verdt å nevne. La oss se på et enkelt eksempel:

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;
    }
}
Mest interessant er det stor sannsynlighet for at dette ikke fungerer. Den nye tråden vil ikke se endringen i flagfeltet. For å fikse dette for flagfeltet, må vi bruke volatilesøkeordet. Hvordan og hvorfor? Prosessoren utfører alle handlingene. Men resultatene av beregninger må lagres et sted. Til dette er det hovedminne og det er prosessorens cache. En prosessors cacher er som en liten del av minnet som brukes til å få tilgang til data raskere enn når du får tilgang til hovedminnet. Men alt har en ulempe: dataene i cachen er kanskje ikke oppdatert (som i eksempelet ovenfor, når verdien av flaggfeltet ikke ble oppdatert). Såvolatilenøkkelordet forteller JVM at vi ikke ønsker å bufre variabelen vår. Dette gjør at det oppdaterte resultatet kan sees på alle tråder. Dette er en svært forenklet forklaring. Når det gjelder søkeordet volatile, anbefaler jeg på det sterkeste at du leser denne artikkelen . For mer informasjon anbefaler jeg deg også å lese Java Memory Model og Java Volatile Keyword . I tillegg er det viktig å huske at det volatilehandler om synlighet, og ikke om atomiteten til endringer. Ser vi på koden i "Race conditions"-delen, vil vi se et verktøytips i IntelliJ IDEA: Bedre sammen: Java og Thread-klassen.  Del III – Interaksjon – 7Denne inspeksjonen ble lagt til IntelliJ IDEA som en del av utgave IDEA-61117 , som ble oppført i versjonsmerknadene tilbake i 2010.

Atomitet

Atomoperasjoner er operasjoner som ikke kan deles. For eksempel må operasjonen med å tilordne en verdi til en variabel være atom. Dessverre er inkrementoperasjonen ikke atomisk, fordi inkrement krever så mange som tre CPU-operasjoner: hent den gamle verdien, legg til en til den, og lagre deretter verdien. Hvorfor er atomitet viktig? Med inkrementoperasjonen, hvis det er en rasebetingelse, kan den delte ressursen (dvs. den delte verdien) plutselig endres når som helst. I tillegg er operasjoner som involverer 64-bits strukturer, for eksempel longog double, ikke atomære. Flere detaljer kan leses her: Sørg for atomitet når du leser og skriver 64-bits verdier . Problemer knyttet til atomitet kan sees i dette eksemplet:

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());
    }
}
Spesialklassen AtomicIntegervil alltid gi oss 30 000, men det valuevil endre seg fra tid til annen. Det er en kort oversikt over dette emnet: Introduksjon til atomvariabler i Java . "Sammenlign-og-bytt"-algoritmen ligger i hjertet av atomklassene. Du kan lese mer om det her i Sammenligning av låsfrie algoritmer - CAS og FAA på eksemplet med JDK 7 og 8 eller i Sammenlign-og-bytt- artikkelen på Wikipedia. Bedre sammen: Java og Thread-klassen.  Del III – Interaksjon – 9

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

Skjer-før

Det er et interessant og mystisk konsept som heter "skjer før". Som en del av studiet av tråder, bør du lese om det. Skjer-før-forholdet viser rekkefølgen handlinger mellom tråder vil bli sett i. Det er mange tolkninger og kommentarer. Her er en av de siste presentasjonene om dette emnet: Java "Happens-Before"-relasjoner .

Sammendrag

I denne anmeldelsen har vi utforsket noen av detaljene for hvordan tråder samhandler. Vi diskuterte problemer som kan oppstå, samt måter å identifisere og eliminere dem. Liste over tilleggsmateriell om emnet: Bedre sammen: Java og Thread-klassen. Del I — Tråder av utførelse Bedre sammen: Java og trådklassen. Del II — Synkronisering Bedre sammen: Java og Thread-klassen. Del IV — Callable, Future og friends Bedre sammen: Java og Thread-klassen. Del V — Executor, ThreadPool, Fork/Join Better together: Java og Thread-klassen. Del VI – Fyr vekk!
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION