Condiții preliminare pentru apariția operațiilor atomice

Să aruncăm o privire la acest exemplu pentru a vă ajuta să înțelegeți cum funcționează operațiile atomice:

public class Counter {
    int count;

    public void increment() {
        count++;
    }
}

Când avem un fir, totul funcționează excelent, dar dacă adăugăm multithreading, obținem rezultate greșite și totul pentru că operația de incrementare nu este o singură operație, ci trei: o solicitare pentru a obține valoarea curentănumara, apoi creșteți-l cu 1 și scrieți din nou înnumara.

Și când două fire de execuție doresc să incrementeze o variabilă, cel mai probabil veți pierde date. Adică, ambele fire primesc 100, ca urmare, ambele vor scrie 101 în loc de valoarea așteptată de 102.

Și cum se rezolvă? Trebuie să folosiți încuietori. Cuvântul cheie sincronizat ajută la rezolvarea acestei probleme, folosirea lui vă oferă garanția că un fir va accesa metoda la un moment dat.

public class SynchronizedCounterWithLock {
    private volatile int count;

    public synchronized void increment() {
        count++;
    }
}

În plus, trebuie să adăugați cuvântul cheie volatil , care asigură vizibilitatea corectă a referințelor între fire. Am trecut în revistă munca lui mai sus.

Dar totuși există dezavantaje. Cel mai mare este performanța, în acel moment în care multe fire încearcă să obțină o blocare și unul primește o oportunitate de scriere, restul firelor fie vor fi blocate, fie suspendate până când firul este eliberat.

Toate aceste procese, blocarea, trecerea la o altă stare sunt foarte costisitoare pentru performanța sistemului.

Operații atomice

Algoritmul folosește instrucțiuni de mașină de nivel scăzut, cum ar fi compare-and-swap (CAS, compare-and-swap, care asigură integritatea datelor și există deja o cantitate mare de cercetări asupra lor).

O operație CAS tipică operează pe trei operanzi:

  • Spațiu de memorie pentru muncă (M)
  • Valoarea așteptată existentă (A) a unei variabile
  • Valoarea nouă (B) trebuie setată

CAS actualizează atomic M la B, dar numai dacă valoarea lui M este aceeași cu A, altfel nu se ia nicio măsură.

În primul și al doilea caz, va fi returnată valoarea lui M. Acest lucru vă permite să combinați trei pași, și anume, obținerea valorii, compararea valorii și actualizarea acesteia. Și totul se transformă într-o singură operațiune la nivel de mașină.

În momentul în care o aplicație cu mai multe fire accesează o variabilă și încearcă să o actualizeze și se aplică CAS, atunci unul dintre fire o va obține și o va putea actualiza. Dar, spre deosebire de blocări, alte fire vor primi pur și simplu erori despre faptul că nu pot actualiza valoarea. Apoi, vor trece la lucrări ulterioare, iar schimbarea este complet exclusă în acest tip de muncă.

În acest caz, logica devine mai dificilă din cauza faptului că trebuie să gestionăm situația în care operațiunea CAS nu a funcționat cu succes. Vom modela codul astfel încât să nu se miște mai departe până când operația va reuși.

Introducere în tipurile atomice

Ați întâlnit o situație în care trebuie să configurați sincronizarea pentru cea mai simplă variabilă de tip int ?

Prima modalitate pe care am abordat-o deja este utilizarea volatile + synchronized . Dar există și cursuri speciale de Atomic*.

Dacă folosim CAS, atunci operațiunile funcționează mai rapid comparativ cu prima metodă. Și în plus, avem metode speciale și foarte convenabile de adăugare a unei valori și operații de creștere și decrementare.

AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray sunt clase în care operațiile sunt atomice. Mai jos vom analiza munca cu ei.

AtomicInteger

Clasa AtomicInteger oferă operații pe o valoare int care poate fi citită și scrisă atomic, în plus față de furnizarea de operații atomice extinse.

Are metode get și set care funcționează ca citirea și scrierea variabilelor.

Adică, „se întâmplă-înainte” cu orice primire ulterioară a aceleiași variabile despre care am vorbit mai devreme. Metoda atomică compareAndSet are și aceste caracteristici de consistență a memoriei.

Toate operațiunile care returnează o nouă valoare sunt efectuate atomic:

int addAndGet (int delta) Adaugă o anumită valoare valorii curente.
boolean compareAndSet(așteptat int, update int) Setează valoarea la valoarea actualizată dată dacă valoarea curentă se potrivește cu valoarea așteptată.
int decrementAndGet() Reduce valoarea curentă cu unu.
int getAndAdd(int delta) Adaugă valoarea dată la valoarea curentă.
int getAndDecrement() Reduce valoarea curentă cu unu.
int getAndIncrement() Mărește valoarea curentă cu unu.
int getAndSet(int newValue) Setează valoarea dată și returnează valoarea veche.
int incrementAndGet() Mărește valoarea curentă cu unu.
lazySet(int newValue) În sfârșit, setați la valoarea dată.
boolean slabCompareAndSet(așteptată, actualizare int) Setează valoarea la valoarea actualizată dată dacă valoarea curentă se potrivește cu valoarea așteptată.

Exemplu:

ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);

System.out.println(atomicInteger.get()); // prints 50