En introduktion til atomvariabler i Java

1. Introduktion

Kort sagt, en delt mutabel tilstand fører meget let til problemer, når samtidighed er involveret. Hvis adgang til delte mutable objekter ikke administreres ordentligt, kan applikationer hurtigt blive udsat for nogle vanskelige at opdage samtidige fejl.

I denne artikel vil vi revidere brugen af ​​låse til at håndtere samtidig adgang, udforske nogle af ulemperne forbundet med låse og endelig introducere atomvariabler som et alternativ.

2. Låse

Lad os se på klassen:

offentlig klassetæller {int counter; offentlig tomrumsforøgelse () {tæller ++; }}

I tilfælde af et enkelt trådmiljø fungerer dette perfekt; så snart vi tillader mere end en tråd at skrive, begynder vi at få inkonsekvente resultater.

Dette skyldes den enkle trinoperation (tæller ++), som kan se ud som en atomoperation, men faktisk er en kombination af tre operationer: opnåelse af værdien, inkrementering og skrivning af den opdaterede værdi tilbage.

Hvis to tråde forsøger at hente og opdatere værdien på samme tid, kan det resultere i mistede opdateringer.

En af måderne til at administrere adgang til et objekt er at bruge låse. Dette kan opnås ved hjælp af synkroniseret nøgleord i stigning metodesignatur. Det synkroniseret nøgleord sikrer, at kun en tråd kan indtaste metoden ad gangen (for at lære mere om låsning og synkronisering henvises til - Vejledning til synkroniseret søgeord i Java):

offentlig klasse SafeCounterWithLock {privat flygtig int-tæller; offentlig synkroniseret tomrumsforøgelse () {tæller ++; }}

Derudover skal vi tilføje flygtige nøgleord for at sikre korrekt reference synlighed blandt tråde.

Brug af låse løser problemet. Imidlertid får forestillingen et hit.

Når flere tråde forsøger at erhverve en lås, vinder en af ​​dem, mens resten af ​​tråde enten er blokeret eller suspenderet.

Processen med at suspendere og derefter genoptage en tråd er meget dyr og påvirker systemets samlede effektivitet.

I et lille program, som f.eks tæller, kan tiden brugt i kontekstskift blive meget mere end den faktiske kodeudførelse, hvilket reducerer den samlede effektivitet.

3. Atomoperationer

Der er en gren af ​​forskning fokuseret på at skabe ikke-blokerende algoritmer til samtidige miljøer. Disse algoritmer udnytter instruktioner på lavt niveau med atomare maskiner såsom sammenligning og udskiftning (CAS) for at sikre dataintegritet.

En typisk CAS-operation fungerer på tre operander:

  1. Hukommelsesplaceringen, hvor den skal betjenes (M)
  2. Den eksisterende forventede værdi (A) for variablen
  3. Den nye værdi (B), der skal indstilles

CAS-operationen opdaterer atomisk værdien i M til B, men kun hvis den eksisterende værdi i M matcher A, ellers foretages der ingen handling.

I begge tilfælde returneres den eksisterende værdi i M. Dette kombinerer tre trin - at få værdien, sammenligne værdien og opdatere værdien - til en enkelt maskinniveauoperation.

Når flere tråde forsøger at opdatere den samme værdi gennem CAS, vinder en af ​​dem og opdaterer værdien. Imidlertid, i modsætning til i tilfælde af låse, bliver ingen anden tråd suspenderet; i stedet informeres de simpelthen om, at de ikke formåede at opdatere værdien. Trådene kan derefter fortsætte med at udføre yderligere arbejde, og kontekstskifter undgås fuldstændigt.

En anden konsekvens er, at kerneprogrammelogikken bliver mere kompleks. Dette skyldes, at vi skal håndtere scenariet, når CAS-operationen ikke lykkedes. Vi kan prøve det igen og igen, indtil det lykkes, eller vi kan ikke gøre noget og gå videre afhængigt af brugssagen.

4. Atomiske variabler i Java

De mest anvendte atomvariabler i Java er AtomicInteger, AtomicLong, AtomicBoolean og AtomicReference. Disse klasser repræsenterer en int, lang, boolsk, og henholdsvis objektreference, som kan opdateres atomisk. De vigtigste metoder, der udsættes for disse klasser, er:

  • få() - får værdien fra hukommelsen, så ændringer foretaget af andre tråde er synlige; svarende til læsning a flygtige variabel
  • sæt() - skriver værdien til hukommelsen, så ændringen er synlig for andre tråde; svarende til at skrive en flygtige variabel
  • lazySet () - til sidst skriver værdien til hukommelsen, måske omorganiseret med efterfølgende relevante hukommelsesoperationer. Én brugssag er at annullere referencer af hensyn til affaldsindsamling, som aldrig kommer til at blive åbnet igen. I dette tilfælde opnås bedre ydeevne ved at forsinke nul flygtige skrive
  • sammenlignAndSet () - det samme som beskrevet i afsnit 3, returnerer sandt, når det lykkes, ellers falsk
  • weakCompareAndSet () - det samme som beskrevet i afsnit 3, men svagere i den forstand, at det ikke skaber hændelser før ordrer. Dette betyder, at det muligvis ikke nødvendigvis kan se opdateringer til andre variabler. Fra og med Java 9 er denne metode udfaset i alle atomimplementeringer til fordel for svagCompareAndSetPlain (). Hukommelseseffekterne af weakCompareAndSet () var almindelige, men dens navne antydede flygtige hukommelseseffekter. For at undgå denne forvirring forældede de denne metode og tilføjede fire metoder med forskellige hukommelseseffekter som f.eks svagCompareAndSetPlain () eller svagCompareAndSetVolatile ()

En trådsikker tæller implementeret med AtomicInteger er vist i eksemplet nedenfor:

offentlig klasse SafeCounterWithoutLock {private final AtomicInteger counter = new AtomicInteger (0); public int getValue () {return counter.get (); } offentlig tomrumsforøgelse () {while (true) {int existingValue = getValue (); int newValue = eksisterende værdi + 1; if (counter.compareAndSet (existingValue, newValue)) {return; }}}}

Som du kan se, prøver vi igen sammenlignAndSet drift og igen ved fiasko, da vi vil garantere, at opkaldet til stigning metode øger altid værdien med 1.

5. Konklusion

I denne hurtige vejledning beskrev vi en alternativ måde at håndtere samtidighed på, hvor ulemper forbundet med låsning kan undgås. Vi kiggede også på de vigtigste metoder, der blev eksponeret af atomvariabler i Java.

Som altid er eksemplerne tilgængelige på GitHub.

For at udforske flere klasser, der internt bruger ikke-blokerende algoritmer, henvises til en guide til ConcurrentMap.