Vereisten voor de opkomst van atomaire operaties

Laten we dit voorbeeld eens bekijken om u te helpen begrijpen hoe atomaire bewerkingen werken:

public class Counter {
    int count;

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

Als we één thread hebben, werkt alles prima, maar als we multithreading toevoegen, krijgen we verkeerde resultaten, en dat allemaal omdat de increment-bewerking niet één bewerking is, maar drie: een verzoek om de huidige waarde te krijgengraaf, verhoog het dan met 1 en schrijf opnieuw naargraaf.

En wanneer twee threads een variabele willen verhogen, verliest u hoogstwaarschijnlijk gegevens. Dat wil zeggen, beide threads ontvangen 100, als resultaat zullen beide 101 schrijven in plaats van de verwachte waarde van 102.

En hoe het op te lossen? Je moet sloten gebruiken. Het gesynchroniseerde trefwoord helpt dit probleem op te lossen, het gebruik ervan geeft u de garantie dat één thread tegelijk toegang krijgt tot de methode.

public class SynchronizedCounterWithLock {
    private volatile int count;

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

Bovendien moet u het vluchtige trefwoord toevoegen , dat zorgt voor de juiste zichtbaarheid van referenties tussen threads. We hebben zijn werk hierboven besproken.

Maar er zijn nog steeds nadelen. De grootste is de prestatie, op het moment dat veel threads een slot proberen te krijgen en één een schrijfmogelijkheid krijgt, wordt de rest van de threads geblokkeerd of opgeschort totdat de thread wordt vrijgegeven.

Al deze processen, blokkeren, overschakelen naar een andere status zijn erg duur voor de systeemprestaties.

Atomaire operaties

Het algoritme maakt gebruik van machine-instructies op laag niveau, zoals Compare-and-Swap (CAS, Compare-and-Swap, dat zorgt voor gegevensintegriteit en er is al veel onderzoek naar gedaan).

Een typische CAS-bewerking werkt op drie operanden:

  • Geheugenruimte voor werk (M)
  • Bestaande verwachte waarde (A) van een variabele
  • Nieuwe waarde (B) in te stellen

CAS werkt M atomair bij naar B, maar alleen als de waarde van M hetzelfde is als A, anders wordt er geen actie ondernomen.

In het eerste en tweede geval wordt de waarde geretourneerd van M. Hiermee kunt u drie stappen combineren, namelijk de waarde ophalen, de waarde vergelijken en bijwerken. En het wordt allemaal één handeling op machineniveau.

Op het moment dat een toepassing met meerdere threads een variabele benadert en deze probeert bij te werken en CAS wordt toegepast, krijgt een van de threads deze en kan deze bijwerken. Maar in tegenstelling tot sloten, krijgen andere threads gewoon fouten over het niet kunnen bijwerken van de waarde. Daarna stromen ze door naar vervolgwerk en is overstappen bij dit soort werk geheel uitgesloten.

In dit geval wordt de logica moeilijker omdat we de situatie moeten aanpakken waarin de CAS-operatie niet succesvol werkte. We modelleren de code gewoon zodat deze pas verder gaat als de bewerking is geslaagd.

Inleiding tot atomaire typen

Bent u een situatie tegengekomen waarin u synchronisatie moet instellen voor de eenvoudigste variabele van het type int ?

De eerste manier die we al hebben behandeld, is het gebruik van vluchtig + gesynchroniseerd . Maar er zijn ook speciale Atomic*-klassen.

Als we CAS gebruiken, werken bewerkingen sneller in vergelijking met de eerste methode. En daarnaast hebben we speciale en zeer handige methoden voor het toevoegen van een waarde en het verhogen en verlagen van bewerkingen.

AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray zijn klassen waarin bewerkingen atomair zijn. Hieronder zullen we het werk met hen analyseren.

AtomicInteger

De klasse AtomicInteger biedt bewerkingen op een int- waarde die atomisch kan worden gelezen en geschreven, naast uitgebreide atomaire bewerkingen.

Het heeft get- en set- methoden die werken zoals het lezen en schrijven van variabelen.

Dat wil zeggen, "gebeurt eerder" met elke volgende ontvangst van dezelfde variabele waar we het eerder over hadden. De atomaire CompareAndSet- methode heeft ook deze functies voor geheugenconsistentie.

Alle bewerkingen die een nieuwe waarde retourneren, worden atomair uitgevoerd:

int addAndGet (int delta) Voegt een specifieke waarde toe aan de huidige waarde.
boolean CompareAndSet (verwachte int, update int) Stelt de waarde in op de gegeven bijgewerkte waarde als de huidige waarde overeenkomt met de verwachte waarde.
int decrementAndGet() Verlaagt de huidige waarde met één.
int getAndAdd(int delta) Telt de gegeven waarde op bij de huidige waarde.
int getAndDecrement() Verlaagt de huidige waarde met één.
int getAndIncrement() Verhoogt de huidige waarde met één.
int getAndSet(int nieuweWaarde) Stelt de gegeven waarde in en retourneert de oude waarde.
int incrementAndGet() Verhoogt de huidige waarde met één.
lazySet(int nieuweWaarde) Eindelijk ingesteld op de opgegeven waarde.
booleaanse zwakkeCompareAndSet(verwacht, update int) Stelt de waarde in op de gegeven bijgewerkte waarde als de huidige waarde overeenkomt met de verwachte waarde.

Voorbeeld:

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