Uvod u atomske varijable u Javi

1. Uvod

Jednostavno rečeno, zajedničko promjenjivo stanje vrlo lako dovodi do problema kada je u pitanju istodobnost. Ako se pristupom dijeljenim promjenjivim objektima ne upravlja pravilno, aplikacije mogu brzo postati podložne nekim teško otkrivenim pogreškama istodobnosti.

U ovom ćemo članku ponovno pregledati upotrebu brava za rukovanje istodobnim pristupom, istražiti neke nedostatke povezane s bravama i na kraju uvesti atomske varijable kao alternativu.

2. Brave

Pogledajmo razred:

brojač javne klase {int brojač; prirast javne praznine () {counter ++; }}

U slučaju jednonitnog okruženja, ovo savršeno funkcionira; međutim, čim dopustimo pisanje više niti, počinjemo dobivati ​​nedosljedne rezultate.

To je zbog jednostavne operacije povećanja (brojač ++), što može izgledati kao atomska operacija, ali zapravo je kombinacija triju operacija: dobivanje vrijednosti, povećanje i zapisivanje ažurirane vrijednosti natrag.

Ako dvije niti pokušavaju istodobno dobiti i ažurirati vrijednost, to može dovesti do izgubljenih ažuriranja.

Jedan od načina upravljanja pristupom objektu je upotreba brava. To se može postići korištenjem sinkronizirano ključna riječ u prirast potpis metode. The sinkronizirano ključna riječ osigurava da samo jedna nit može istovremeno ući u metodu (da biste saznali više o zaključavanju i sinkronizaciji pogledajte - Vodič za sinkroniziranu ključnu riječ na Javi):

javna klasa SafeCounterWithLock {private volatile int counter; javni sinkronizirani priraštaj praznine () {brojač ++; }}

Uz to moramo dodati i hlapljiv ključna riječ kako bi se osigurala odgovarajuća vidljivost reference među nitima.

Korištenje brava rješava problem. Međutim, izvedba zahtijeva hit.

Kada više niti pokušava postići bravu, jedna od njih pobjeđuje, dok su ostale niti blokirane ili suspendirane.

Postupak suspendiranja i ponovnog pokretanja niti vrlo je skup a utječe na ukupnu učinkovitost sustava.

U malom programu, kao što je brojač, vrijeme provedeno u prebacivanju konteksta može postati mnogo više od stvarnog izvršavanja koda, što uvelike smanjuje ukupnu učinkovitost.

3. Atomske operacije

Postoji grana istraživanja usmjerena na stvaranje neblokirajućih algoritama za istodobna okruženja. Ovi algoritmi koriste upute za atomske strojeve niske razine, poput usporedbe i zamjene (CAS), kako bi osigurali cjelovitost podataka.

Tipična CAS operacija djeluje na tri operanda:

  1. Memorijsko mjesto na kojem treba raditi (M)
  2. Postojeća očekivana vrijednost (A) varijable
  3. Nova vrijednost (B) koju treba postaviti

CAS operacija atomsko ažurira vrijednost u M na B, ali samo ako se postojeća vrijednost u M podudara s A, inače se ne poduzimaju radnje.

U oba slučaja vraća se postojeća vrijednost u M. Ovo kombinira tri koraka - dobivanje vrijednosti, usporedbu vrijednosti i ažuriranje vrijednosti - u jednu operaciju na razini stroja.

Kad više niti pokušava ažurirati istu vrijednost putem CAS-a, jedna od njih pobjeđuje i ažurira vrijednost. Međutim, za razliku od brave, niti jedna druga niti se ne suspendira; umjesto toga, jednostavno su obaviješteni da nisu uspjeli ažurirati vrijednost. Zatim niti mogu nastaviti s daljnjim radom i prekidači konteksta su potpuno izbjegnuti.

Još jedna posljedica je da logika temeljnog programa postaje složenija. To je zato što moramo riješiti scenarij kada CAS operacija nije uspjela. Možemo pokušati iznova i iznova dok to ne uspije ili ne možemo poduzeti ništa i krenuti dalje, ovisno o slučaju upotrebe.

4. Atomske varijable u Javi

Najčešće korištene klase atomskih varijabli u Javi su AtomicInteger, AtomicLong, AtomicBoolean i AtomicReference. Ove klase predstavljaju int, dugo, boolean, odnosno referenca na objekt koji se mogu atomski ažurirati. Glavne metode koje izlažu ove klase su:

  • dobiti() - dobiva vrijednost iz memorije, tako da su vidljive promjene koje su napravile druge niti; ekvivalentno čitanju a hlapljiv varijabilna
  • postavi () - zapisuje vrijednost u memoriju, tako da je promjena vidljiva ostalim nitima; ekvivalentno pisanju a hlapljiv varijabilna
  • lazySet () - na kraju zapisuje vrijednost u memoriju, možda preuređenu s naknadnim relevantnim memorijskim operacijama. Jedan slučaj upotrebe su poništavanje referenci, zbog odvoza smeća, kojem se više nikada neće pristupiti. U ovom se slučaju bolja izvedba postiže odgađanjem nule hlapljiv pisati
  • compareAndSet () - isto kao što je opisano u odjeljku 3, vraća true kada uspije, inače false
  • slabCompareAndSet () - isto kao što je opisano u odjeljku 3, ali slabije u smislu da se ne stvara prije narudžbi. To znači da možda neće nužno vidjeti ažuriranja drugih varijabli. Od Jave 9, ova je metoda zastarjela u svim atomskim implementacijama u korist slabCompareAndSetPlain (). Efekti pamćenja slabCompareAndSet () bili su obični, ali njegova su imena implicirala hlapljive efekte pamćenja. Da bi izbjegli ovu zabunu, odbacili su ovu metodu i dodali su četiri metode s različitim memorijskim efektima kao što su slabCompareAndSetPlain () ili slabCompareAndSetVolatile ()

Brojač zaštićen nitima implementiran s AtomicInteger prikazan je u donjem primjeru:

javna klasa SafeCounterWithoutLock {privatni konačni brojač AtomicInteger = novi AtomicInteger (0); javni int getValue () {return counter.get (); } javni void inkrement () {while (true) {int existingValue = getValue (); int newValue = postojeća vrijednost + 1; if (counter.compareAndSet (existingValue, newValue)) {return; }}}}

Kao što vidite, pokušavamo ponovo compareAndSet operacija i opet u slučaju kvara, jer želimo jamčiti da će poziv na prirast metoda uvijek povećava vrijednost za 1.

5. Zaključak

U ovom smo brzom vodiču opisali alternativni način rukovanja paralelnošću gdje se mogu izbjeći nedostaci povezani s zaključavanjem. Također smo pogledali glavne metode koje izlažu klase atomskih varijabli u Javi.

Kao i uvijek, primjeri su dostupni na GitHubu.

Da biste istražili više klasa koje interno koriste neblokirajuće algoritme, pogledajte vodič za ConcurrentMap.