En introduksjon til atomvariabler i Java

1. Introduksjon

Enkelt sagt, en delt mutabel tilstand fører lett til problemer når samtidig er involvert. Hvis tilgang til delte mutable objekter ikke administreres riktig, kan applikasjoner raskt bli utsatt for noen vanskelige å oppdage samtidighetsfeil.

I denne artikkelen vil vi se på bruken av låser for å håndtere samtidig tilgang, utforske noen av ulempene knyttet til låser, og til slutt introdusere atomvariabler som et alternativ.

2. Låser

La oss ta en titt på klassen:

public class Counter {int counter; offentlig ugyldig økning () {counter ++; }}

Når det gjelder et enkelt trådmiljø, fungerer dette perfekt; så snart vi tillater mer enn en tråd å skrive, begynner vi å få inkonsekvente resultater.

Dette er på grunn av den enkle trinnoperasjonen (teller ++), som kan se ut som en atomoperasjon, men faktisk er en kombinasjon av tre operasjoner: å skaffe verdien, inkrementere og skrive den oppdaterte verdien tilbake.

Hvis to tråder prøver å hente og oppdatere verdien samtidig, kan det føre til tapte oppdateringer.

En av måtene å administrere tilgang til et objekt er å bruke låser. Dette kan oppnås ved å bruke synkronisert nøkkelord i økning metodesignatur. De synkronisert nøkkelord sikrer at bare en tråd kan angi metoden om gangen (for å lære mer om låsing og synkronisering, se - Guide to Synchronized Keyword in Java):

offentlig klasse SafeCounterWithLock {private volatile int counter; offentlig synkronisert ugyldig økning () {counter ++; }}

I tillegg må vi legge til flyktige nøkkelord for å sikre riktig referansesynlighet blant tråder.

Bruk av låser løser problemet. Imidlertid tar forestillingen en hit.

Når flere tråder prøver å skaffe seg en lås, vinner en av dem, mens resten av trådene enten er blokkert eller suspendert.

Prosessen med å suspendere og deretter gjenoppta en tråd er veldig kostbar og påvirker systemets samlede effektivitet.

I et lite program, som f.eks disk, kan tiden som brukes i kontekstbytte bli mye mer enn faktisk kodeutførelse, og dermed redusere den totale effektiviteten.

3. Atomiske operasjoner

Det er en gren av forskning fokusert på å lage ikke-blokkerende algoritmer for samtidige miljøer. Disse algoritmene utnytter instruksjoner på atomnivå på lavt nivå, for eksempel sammenligning og bytte (CAS), for å sikre dataintegritet.

En typisk CAS-operasjon fungerer på tre operander:

  1. Minneplasseringen du skal bruke (M)
  2. Den eksisterende forventede verdien (A) av variabelen
  3. Den nye verdien (B) som må stilles inn

CAS-operasjonen oppdaterer atomisk verdien i M til B, men bare hvis den eksisterende verdien i M samsvarer med A, ellers blir det ikke gjort noe.

I begge tilfeller returneres den eksisterende verdien i M. Dette kombinerer tre trinn - å få verdien, sammenligne verdien og oppdatere verdien - til en enkelt maskinnivåoperasjon.

Når flere tråder prøver å oppdatere den samme verdien gjennom CAS, vinner en av dem og oppdaterer verdien. Imidlertid, i motsetning til i tilfelle låser, blir ingen annen tråd suspendert; i stedet blir de bare informert om at de ikke klarte å oppdatere verdien. Trådene kan deretter fortsette å gjøre videre arbeid, og kontekstbrytere unngås helt.

En annen konsekvens er at kjerneprogrammelogikken blir mer kompleks. Dette er fordi vi må håndtere scenariet når CAS-operasjonen ikke lyktes. Vi kan prøve det igjen og igjen til det lykkes, eller vi kan ikke gjøre noe og gå videre, avhengig av brukssaken.

4. Atomiske variabler i Java

De mest brukte atomvariabler i Java er AtomicInteger, AtomicLong, AtomicBoolean og AtomicReference. Disse klassene representerer en int, lang, boolsk, og henholdsvis objektreferanse som kan oppdateres atomisk. De viktigste metodene som eksponeres av disse klassene er:

  • få() - får verdien fra minnet, slik at endringer gjort av andre tråder er synlige; tilsvarer å lese en flyktige variabel
  • sett() - skriver verdien til minnet, slik at endringen er synlig for andre tråder; tilsvarer å skrive en flyktige variabel
  • lazySet () - skriver til slutt verdien til minnet, kanskje omorganisert med påfølgende relevante minneoperasjoner. Én brukssak er å oppheve referanser, for å samle søppel, som aldri kommer til å bli tilgjengelig igjen. I dette tilfellet oppnås bedre ytelse ved å forsinke null flyktige skrive
  • sammenlignAndSet () - samme som beskrevet i avsnitt 3, returnerer sant når det lykkes, ellers falskt
  • weakCompareAndSet () - det samme som beskrevet i avsnitt 3, men svakere i den forstand at det ikke skaper skjer-før bestillinger. Dette betyr at det ikke nødvendigvis ser oppdateringer gjort til andre variabler. Fra og med Java 9 har denne metoden blitt avviklet i alle atomimplementeringer til fordel for weakCompareAndSetPlain (). Hukommelseseffektene av weakCompareAndSet () var enkle, men navnene antydet ustabile minneeffekter. For å unngå denne forvirringen avskaffet de denne metoden og la til fire metoder med forskjellige minneeffekter som f.eks weakCompareAndSetPlain () eller weakCompareAndSetVolatile ()

En trådsikker teller implementert med AtomicInteger er vist i eksemplet nedenfor:

offentlig klasse SafeCounterWithoutLock {private final AtomicInteger counter = new AtomicInteger (0); public int getValue () {return counter.get (); } offentlig tomromstigning () {while (true) {int existingValue = getValue (); int newValue = eksisterende verdi + 1; if (counter.compareAndSet (existingValue, newValue)) {return; }}}}

Som du kan se, prøver vi på nytt sammenlignAndSet drift og igjen ved feil, siden vi vil garantere at samtalen til økning metoden øker alltid verdien med 1.

5. Konklusjon

I denne raske opplæringen beskrev vi en alternativ måte å håndtere samtidighet der ulemper forbundet med låsing kan unngås. Vi så også på hovedmetodene eksponert av atomvariabelklassene i Java.

Som alltid er eksemplene tilgjengelige på GitHub.

For å utforske flere klasser som internt bruker ikke-blokkerende algoritmer, se en guide til ConcurrentMap.


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