Förutsättningar för uppkomsten av atomära operationer
Låt oss ta en titt på det här exemplet för att hjälpa dig förstå hur atomära operationer fungerar:
public class Counter {
int count;
public void increment() {
count++;
}
}
När vi har en tråd fungerar allt utmärkt, men om vi lägger till multithreading får vi fel resultat, och allt eftersom inkrementoperationen inte är en operation, utan tre: en begäran om att få det aktuella värdeträkna, öka den sedan med 1 och skriv igen tillräkna.
Och när två trådar vill öka en variabel kommer du med största sannolikhet att förlora data. Det vill säga att båda trådarna får 100, som ett resultat kommer båda att skriva 101 istället för det förväntade värdet på 102.
Och hur löser man det? Du måste använda lås. Det synkroniserade nyckelordet hjälper till att lösa detta problem, att använda det ger dig garantin att en tråd kommer åt metoden åt gången.
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
Dessutom måste du lägga till det flyktiga nyckelordet , vilket säkerställer korrekt synlighet av referenser bland trådar. Vi har granskat hans arbete ovan.
Men det finns fortfarande nackdelar. Den största är prestanda, vid den tidpunkten när många trådar försöker få ett lås och man får en skrivmöjlighet, kommer resten av trådarna antingen att blockeras eller avbrytas tills tråden släpps.
Alla dessa processer, blockering, byte till en annan status är mycket dyra för systemets prestanda.
Atomverksamhet
Algoritmen använder maskininstruktioner på låg nivå såsom compare-and-swap (CAS, compare-and-swap, vilket säkerställer dataintegritet och det finns redan en stor mängd forskning om dem).
En typisk CAS-operation fungerar på tre operander:
- Minnesutrymme för arbete (M)
- Befintligt förväntat värde (A) för en variabel
- Nytt värde (B) ska ställas in
CAS uppdaterar atomärt M till B, men bara om värdet på M är detsamma som A, annars vidtas ingen åtgärd.
I det första och andra fallet returneras värdet på M. Detta gör att du kan kombinera tre steg, nämligen att hämta värdet, jämföra värdet och uppdatera det. Och det hela blir till en operation på maskinnivå.
I samma ögonblick som en flertrådad applikation kommer åt en variabel och försöker uppdatera den och CAS tillämpas, då kommer en av trådarna att hämta den och kunna uppdatera den. Men till skillnad från lås kommer andra trådar helt enkelt att få fel om att de inte kan uppdatera värdet. Sedan går de vidare till vidare arbete och byte är helt uteslutet i den här typen av arbete.
I det här fallet blir logiken svårare på grund av att vi måste hantera situationen när CAS-operationen inte fungerade framgångsrikt. Vi kommer bara att modellera koden så att den inte går vidare förrän operationen lyckas.
Introduktion till atomtyper
Har du stött på en situation där du behöver ställa in synkronisering för den enklaste variabeln av typen int ?
Det första sättet vi redan har täckt är att använda volatile + synchronized . Men det finns också speciella Atomic*-klasser.
Om vi använder CAS så fungerar operationer snabbare jämfört med den första metoden. Och dessutom har vi speciella och mycket bekväma metoder för att lägga till ett värde och öka och minska operationer.
AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray är klasser där operationerna är atomära. Nedan kommer vi att analysera arbetet med dem.
AtomicInteger
Klassen AtomicInteger tillhandahåller operationer på ett int- värde som kan läsas och skrivas atomärt, förutom att tillhandahålla utökade atomoperationer.
Den har få och ställ in metoder som fungerar som läs- och skrivvariabler.
Det vill säga "händer-före" med varje efterföljande mottagande av samma variabel som vi pratade om tidigare. Atomic compareAndSet- metoden har också dessa minneskonsistensfunktioner.
Alla operationer som returnerar ett nytt värde utförs atomärt:
int addAndGet (int delta) | Lägger till ett specifikt värde till det aktuella värdet. |
boolean compareAndSet(förväntad int, uppdatera int) | Ställer in värdet till det givna uppdaterade värdet om det aktuella värdet matchar det förväntade värdet. |
int decrementAndGet() | Minskar det aktuella värdet med ett. |
int getAndAdd(int delta) | Lägger till det givna värdet till det aktuella värdet. |
int getAndDecrement() | Minskar det aktuella värdet med ett. |
int getAndIncrement() | Ökar det aktuella värdet med ett. |
int getAndSet(int newValue) | Ställer in det givna värdet och returnerar det gamla värdet. |
int incrementAndGet() | Ökar det aktuella värdet med ett. |
lazySet(int newValue) | Ställ slutligen till det angivna värdet. |
boolean weakCompareAndSet(expected, update int) | Ställer in värdet till det givna uppdaterade värdet om det aktuella värdet matchar det förväntade värdet. |
Exempel:
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
GO TO FULL VERSION