Hva er trådsikkerhet og hvordan kan jeg oppnå det?

1. Oversikt

Java støtter multitrading ut av boksen. Dette betyr at ved å kjøre bytecode samtidig i separate arbeidertråder, er JVM i stand til å forbedre applikasjonsytelsen.

Selv om multitrading er en kraftig funksjon, har den en pris. I flertrådede miljøer trenger vi å skrive implementeringer på en trådsikker måte. Dette betyr at forskjellige tråder kan få tilgang til de samme ressursene uten å avsløre feil oppførsel eller gi uforutsigbare resultater. Denne programmeringsmetoden er kjent som “trådsikkerhet”.

I denne opplæringen vil vi se på forskjellige tilnærminger for å oppnå det.

2. Statsløse implementeringer

I de fleste tilfeller er feil i flertrådede applikasjoner et resultat av feil delingsstatus mellom flere tråder.

Derfor er den første tilnærmingen vi skal se på å oppnå trådsikkerhet ved hjelp av statsløse implementeringer.

For å bedre forstå denne tilnærmingen, la oss vurdere en enkel nytteklasse med en statisk metode som beregner faktoren til et tall:

offentlig klasse MathUtils {offentlig statisk BigInteger-faktor (int-nummer) {BigInteger f = nytt BigInteger ("1"); for (int i = 2; i <= nummer; i ++) {f = f.multiply (BigInteger.valueOf (i)); } returner f; }} 

De fabrikk () metoden er en statsløs deterministisk funksjon. Gitt en bestemt inngang, gir den alltid den samme utgangen.

Metoden verken er avhengig av ekstern tilstand eller opprettholder stat i det hele tatt. Derfor anses det å være trådsikkert og kan trygt kalles av flere tråder samtidig.

Alle tråder kan trygt ringe fabrikk () metode og vil få det forventede resultatet uten å forstyrre hverandre og uten å endre utdata som metoden genererer for andre tråder.

Derfor, statsløse implementeringer er den enkleste måten å oppnå trådsikkerhet på.

3. Uforanderlige implementeringer

Hvis vi trenger å dele tilstand mellom forskjellige tråder, kan vi lage trådsikre klasser ved å gjøre dem uforanderlige.

Uforanderlighet er et kraftig, språkagnostisk konsept, og det er ganske enkelt å oppnå i Java.

For å si det enkelt, en klasseinstans er uforanderlig når den interne tilstanden ikke kan endres etter at den er konstruert.

Den enkleste måten å lage en uforanderlig klasse på Java er ved å erklære alle feltene privat og endelig og ikke gi settere:

public class MessageService {private final Strengmelding; public MessageService (strengmelding) {this.message = message; } // standard getter}

EN MessageService objektet er effektivt uforanderlig siden dets tilstand ikke kan endres etter konstruksjonen. Derfor er det trådsikkert.

Videre, hvis MessageService var faktisk mutbare, men flere tråder har bare skrivebeskyttet tilgang til den, den er også trådsikker.

Og dermed, uforanderlighet er bare en annen måte å oppnå trådsikkerhet på.

4. Tråd-lokale felt

I objektorientert programmering (OOP) trenger objekter faktisk å opprettholde tilstand gjennom felt og implementere atferd gjennom en eller flere metoder.

Hvis vi faktisk trenger å opprettholde staten, vi kan lage trådsikre klasser som ikke deler tilstand mellom tråder ved å gjøre feltene sine trådlokale.

Vi kan enkelt lage klasser hvis felt er trådlokale ved ganske enkelt å definere private felt i Tråd klasser.

Vi kunne for eksempel definere en Tråd klasse som lagrer en array av heltall:

public class ThreadA utvider Thread {private final List numbers = Arrays.asList (1, 2, 3, 4, 5, 6); @ Override public void run () {numbers.forEach (System.out :: println); }}

Mens en annen kan ha en array av strenger:

public class ThreadB utvider Thread {private final Listebokstaver = Arrays.asList ("a", "b", "c", "d", "e", "f"); @ Override public void run () {letters.forEach (System.out :: println); }}

I begge implementeringene har klassene sin egen tilstand, men den deles ikke med andre tråder. Dermed er klassene trådsikre.

På samme måte kan vi lage tråd-lokale felt ved å tilordne Trådlokal forekomster til et felt.

La oss for eksempel vurdere følgende StateHolder klasse:

offentlig klasse StateHolder {privat final Strengstat; // standard konstruktører / getter}

Vi kan enkelt gjøre det til en tråd-lokal variabel som følger:

public class ThreadState {public static final ThreadLocal statePerThread = new ThreadLocal () {@Override protected StateHolder initialValue () {return new StateHolder ("active"); }}; offentlig statisk StateHolder getState () {return statePerThread.get (); }}

Trådlokale felt er omtrent som normale klassefelt, bortsett fra at hver tråd som får tilgang til dem via en setter / getter får en uavhengig initialisert kopi av feltet slik at hver tråd har sin egen tilstand.

5. Synkroniserte samlinger

Vi kan enkelt lage trådsikre samlinger ved å bruke settet med synkroniseringspakker som er inkludert i samlingens rammeverk.

Vi kan for eksempel bruke en av disse synkroniseringsinnpakningene for å lage en trådsikker samling:

Collection syncCollection = Collections.synchronizedCollection (ny ArrayList ()); Trådtråd1 = ny tråd (() -> syncCollection.addAll (Arrays.asList (1, 2, 3, 4, 5, 6))); Trådtråd2 = ny tråd (() -> syncCollection.addAll (Arrays.asList (7, 8, 9, 10, 11, 12))); thread1.start (); thread2.start (); 

La oss huske på at synkroniserte samlinger bruker egenlåsing i hver metode (vi vil se på egenlåsing senere).

Dette betyr at metodene bare kan nås av en tråd om gangen, mens andre tråder vil bli blokkert til metoden blir låst opp av den første tråden.

Dermed har synkronisering en straff i ytelse på grunn av den underliggende logikken med synkronisert tilgang.

6. Samtidige samlinger

Alternativt til synkroniserte samlinger, kan vi bruke samtidige samlinger for å lage trådsikre samlinger.

Java tilbyr java.util.concurrent pakke, som inneholder flere samtidige samlinger, for eksempel ConcurrentHashMap:

Kart concurrentMap = ny ConcurrentHashMap (); concurrentMap.put ("1", "one"); concurrentMap.put ("2", "two"); concurrentMap.put ("3", "tre"); 

I motsetning til deres synkroniserte kolleger, samtidige samlinger oppnår trådsikkerhet ved å dele dataene i segmenter. I en ConcurrentHashMapfor eksempel kan flere tråder skaffe seg låser på forskjellige kartsegmenter, slik at flere tråder kan få tilgang til Kart samtidig.

Samtidige samlinger ermye mer performant enn synkroniserte samlinger, på grunn av de iboende fordelene med samtidig trådtilgang.

Det er verdt å nevne det synkroniserte og samtidige samlinger gjør bare selve samlingen trådsikker og ikke innholdet.

7. Atomic Objects

Det er også mulig å oppnå trådsikkerhet ved hjelp av settet med atomklasser som Java gir, inkludert AtomicInteger, AtomicLong, AtomicBoolean, og AtomicReference.

Atomklasser tillater oss å utføre atomoperasjoner, som er trådsikre, uten å bruke synkronisering. En atomoperasjon utføres i en enkelt maskinnivåoperasjon.

For å forstå problemet dette løser, la oss se på følgende Disk klasse:

offentlig klasseteller {privat int-teller = 0; public void incrementCounter () {counter + = 1; } public int getCounter () {return counter; }}

La oss anta at i en løpstilstand har to tråder tilgang til incrementCounter () metode samtidig.

I teorien er den endelige verdien av disk feltet vil være 2. Men vi kan bare ikke være sikre på resultatet, fordi trådene utfører samme kodeblokk samtidig og inkrementering er ikke atomisk.

La oss lage en trådsikker implementering av Disk klasse ved å bruke en AtomicInteger gjenstand:

offentlig klasse AtomicCounter {private final AtomicInteger counter = new AtomicInteger (); public void incrementCounter () {counter.incrementAndGet (); } public int getCounter () {return counter.get (); }}

Dette er trådsikkert fordi, mens inkrementering, ++, tar mer enn én operasjon, inkrementAndGet er atomisk.

8. Synkroniserte metoder

Mens de tidligere tilnærmingene er veldig bra for samlinger og primitiver, vil vi til tider trenge større kontroll enn det.

Så, en annen vanlig tilnærming som vi kan bruke for å oppnå trådsikkerhet, er å implementere synkroniserte metoder.

For å si det enkelt, bare en tråd kan få tilgang til en synkronisert metode om gangen mens den blokkerer tilgangen til denne metoden fra andre tråder. Andre tråder vil forbli blokkert til den første tråden er ferdig, eller metoden gir et unntak.

Vi kan lage en trådsikker versjon av incrementCounter () på en annen måte ved å gjøre det til en synkronisert metode:

offentlig synkronisert ugyldig incrementCounter () {counter + = 1; }

Vi har opprettet en synkronisert metode ved å prefiks metodesignaturen med synkronisert nøkkelord.

Siden en tråd om gangen har tilgang til en synkronisert metode, vil en tråd utføre incrementCounter () metode, og i sin tur vil andre gjøre det samme. Ingen overlappende utførelse vil forekomme overhodet.

Synkroniserte metoder er avhengige av bruken av "indre låser" eller "skjermlåser". En egenlås er en implisitt intern enhet tilknyttet en bestemt klasseinstans.

I en flertrådet sammenheng, begrepet Observere er bare en referanse til rollen som låsen utfører på det tilknyttede objektet, da den håndhever eksklusiv tilgang til et sett med spesifiserte metoder eller utsagn.

Når en tråd kaller en synkronisert metode, får den den indre låsen. Etter at tråden er ferdig med å utføre metoden, frigjør den låsen, slik at andre tråder kan skaffe seg låsen og få tilgang til metoden.

Vi kan implementere synkronisering i eksempelvis metoder, statiske metoder og utsagn (synkroniserte utsagn).

9. Synkroniserte uttalelser

Noen ganger kan det være for mye å synkronisere en hel metode hvis vi bare trenger å gjøre et segment av metoden trådsikker.

For å eksemplifisere denne brukssaken, la oss omformere incrementCounter () metode:

public void incrementCounter () {// ytterligere usynkroniserte operasjoner synkronisert (dette) {counter + = 1; }}

Eksemplet er trivielt, men det viser hvordan du lager en synkronisert uttalelse. Forutsatt at metoden nå utfører noen få ekstra operasjoner, som ikke krever synkronisering, synkroniserte vi bare den aktuelle delmodifiserende delen ved å pakke den inn i en synkronisert blokkere.

I motsetning til synkroniserte metoder, må synkroniserte utsagn spesifisere objektet som gir den indre låsen, vanligvis dette henvisning.

Synkronisering er dyrt, så med dette alternativet kan vi bare synkronisere de relevante delene av en metode.

9.1. Andre gjenstander som en lås

Vi kan forbedre den trådsikre implementeringen av Disk klasse ved å utnytte et annet objekt som skjermlås, i stedet for dette.

Ikke bare gir dette koordinert tilgang til en delt ressurs i et flertrådet miljø, men den bruker også en ekstern enhet for å håndheve eksklusiv tilgang til ressursen:

offentlig klasse ObjectLockCounter {private int counter = 0; privat slutt Objektlås = nytt Objekt (); public void incrementCounter () {synkronisert (lås) {counter + = 1; }} // standard getter}

Vi bruker en slette Gjenstand eksempel for å håndheve gjensidig ekskludering. Denne implementeringen er litt bedre, da den fremmer sikkerhet på låsenivå.

Når du bruker dette for indre låsing, en angriper kan forårsake fastlåst tilstand ved å skaffe seg den indre låsen og utløse en denial of service (DoS) -tilstand.

Tvert imot, når du bruker andre gjenstander, at privat enhet ikke er tilgjengelig utenfra. Dette gjør det vanskeligere for en angriper å skaffe seg låsen og forårsake en fastlåst situasjon.

9.2. Advarsler

Selv om vi kan bruke hvilket som helst Java-objekt som en egen lås, bør vi unngå å bruke Strenger for låseformål:

offentlig klasse Class1 {privat statisk finale String LOCK = "Lås"; // bruker LOCK som den indre låsen} public class Class2 {private static final String LOCK = "Lock"; // bruker LOCK som den indre låsen}

Ved første øyekast ser det ut til at disse to klassene bruker to forskjellige objekter som lås. Derimot, På grunn av strenginterning kan disse to "Lock" -verdiene faktisk referere til det samme objektet i strengbassenget. Det er det Klasse1 og Klasse2 deler samme lås!

Dette kan igjen føre til uventet oppførsel i samtidige sammenhenger.

I tillegg til Strenger, vi bør unngå å bruke gjenstander som kan caches eller gjenbrukes som indre låser. For eksempel Integer.valueOf () metoden cacher små tall. Derfor ringer Integer.valueOf (1) returnerer det samme objektet selv i forskjellige klasser.

10. Flyktige felt

Synkroniserte metoder og blokker er nyttige for å løse problemer med variabel synlighet blant tråder. Likevel kan verdiene til vanlige klassefelt bli bufret av CPUen. Derfor kan det hende at oppdateringer til et bestemt felt, selv om de er synkronisert, kanskje ikke er synlige for andre tråder.

For å forhindre denne situasjonen kan vi bruke flyktige klasse felt:

public class Counter {private volatile int counter; // standard konstruktører / getter}

Med flyktige nøkkelord, vi instruerer JVM og kompilatoren om å lagre disk variabel i hovedminnet. På den måten sørger vi for at hver gang JVM leser verdien av disk variabel, vil den faktisk lese den fra hovedminnet, i stedet for fra CPU-hurtigbufferen. Likeledes hver gang JVM skriver til disk variabel, blir verdien skrevet til hovedminnet.

Videre bruken av en flyktige variabel sørger for at alle variabler som er synlige for en gitt tråd også blir lest fra hovedminnet.

La oss vurdere følgende eksempel:

offentlig klasse bruker {privat strengnavn; privat flyktig alder; // standard konstruktører / getters}

I dette tilfellet, hver gang JVM skriver alderflyktige variabelt til hovedminnet, vil det skrive det ikke-flyktige Navn variabel til hovedminnet også. Dette forsikrer at de siste verdiene til begge variablene er lagret i hovedminnet, slik at oppdateringer av variablene automatisk vil være synlige for andre tråder.

På samme måte, hvis en tråd leser verdien av en flyktige variabel, vil alle variablene som er synlige for tråden også bli lest fra hovedminnet.

Denne utvidede garantien for det flyktige variabler gir er kjent som full garanti for ustabil synlighet.

11. Reentrant Låser

Java gir et forbedret sett med Låse implementeringer, hvis oppførsel er litt mer sofistikert enn de indre låser som er diskutert ovenfor.

Med indre låser er modellen for låseanskaffelse ganske stiv: en tråd henter låsen, utfører deretter en metode eller kodeblokk, og til slutt frigjør låsen, slik at andre tråder kan få den og få tilgang til metoden.

Det er ingen underliggende mekanisme som kontrollerer trådene i kø og gir prioritet tilgang til de lengste ventende trådene.

ReentrantLock tilfeller tillater oss å gjøre akkurat det, derav å forhindre at trådene i kø lider av noen typer ressurssult:

offentlig klasse ReentrantLockCounter {private int counter; privat finale ReentrantLock reLock = ny ReentrantLock (true); public void incrementCounter () {reLock.lock (); prøv {counter + = 1; } til slutt {reLock.unlock (); }} // standard konstruktører / getter}

De ReentrantLock konstruktør tar et valgfritt rettferdighetboolsk parameter. Når satt til ekte, og flere tråder prøver å skaffe seg en lås, JVM vil prioritere den lengste ventetråden og gi tilgang til låsen.

12. Les / skriv lås

En annen kraftig mekanisme som vi kan bruke for å oppnå trådsikkerhet er bruken av ReadWriteLock implementeringer.

EN ReadWriteLock lock bruker faktisk et par tilknyttede låser, en for skrivebeskyttet operasjon og en annen for skriveoperasjoner.

Som et resultat, det er mulig å ha mange tråder som leser en ressurs, så lenge det ikke er noen tråd som skriver til den. Videre vil tråden som skrives til ressursen forhindre at andre tråder leser den.

Vi kan bruke en ReadWriteLock låse som følger:

offentlig klasse ReentrantReadWriteLockCounter {private int counter; privat slutt ReentrantReadWriteLock rwLock = ny ReentrantReadWriteLock (); privat slutt Lås readLock = rwLock.readLock (); privat slutt Lås skriveLås = rwLock.writeLock (); public void incrementCounter () {writeLock.lock (); prøv {counter + = 1; } til slutt {writeLock.unlock (); }} offentlig int getCounter () {readLock.lock (); prøv {retur teller; } til slutt {readLock.unlock (); }} // standardkonstruktører} 

13. Konklusjon

I denne artikkelen, vi lærte hva trådsikkerhet er i Java, og så grundig på ulike tilnærminger for å oppnå det.

Som vanlig er alle kodeeksemplene vist i denne artikkelen tilgjengelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found