Forudsætninger for fremkomsten af atomare operationer
Lad os tage et kig på dette eksempel for at hjælpe dig med at forstå, hvordan atomariske operationer fungerer:
public class Counter {
int count;
public void increment() {
count++;
}
}
Når vi har én tråd, fungerer alt godt, men hvis vi tilføjer multithreading, får vi forkerte resultater, og alt sammen fordi inkrementoperationen ikke er én operation, men tre: en anmodning om at få den aktuelle værditælle, øg den derefter med 1 og skriv igen tiltælle.
Og når to tråde ønsker at øge en variabel, vil du højst sandsynligt miste data. Det vil sige, at begge tråde modtager 100, som et resultat vil begge skrive 101 i stedet for den forventede værdi på 102.
Og hvordan løses det? Du skal bruge låse. Det synkroniserede søgeord hjælper med at løse dette problem, ved at bruge det giver dig garanti for, at én tråd får adgang til metoden ad gangen.
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
Derudover skal du tilføje det flygtige søgeord , som sikrer den korrekte synlighed af referencer blandt tråde. Vi har gennemgået hans arbejde ovenfor.
Men der er stadig ulemper. Den største er ydeevne, på det tidspunkt, hvor mange tråde forsøger at få en lås, og man får en skrivemulighed, vil resten af trådene enten blive blokeret eller suspenderet, indtil tråden frigives.
Alle disse processer, blokering, skift til en anden status er meget dyre for systemets ydeevne.
Atomiske operationer
Algoritmen bruger maskininstruktioner på lavt niveau, såsom compare-and-swap (CAS, compare-and-swap, som sikrer dataintegritet, og der er allerede en stor mængde forskning på dem).
En typisk CAS-operation fungerer på tre operander:
- Hukommelsesplads til arbejde (M)
- Eksisterende forventet værdi (A) af en variabel
- Ny værdi (B) skal indstilles
CAS opdaterer atomisk M til B, men kun hvis værdien af M er den samme som A, ellers foretages der ingen handling.
I det første og andet tilfælde returneres værdien af M. Dette giver dig mulighed for at kombinere tre trin, nemlig at få værdien, sammenligne værdien og opdatere den. Og det hele bliver til én operation på maskinniveau.
I det øjeblik en multi-threaded applikation får adgang til en variabel og forsøger at opdatere den, og CAS anvendes, så vil en af trådene få den og være i stand til at opdatere den. Men i modsætning til låse vil andre tråde simpelthen få fejl om ikke at kunne opdatere værdien. Så går de videre til videre arbejde, og skift er helt udelukket i denne type arbejde.
I dette tilfælde bliver logikken sværere på grund af det faktum, at vi skal håndtere situationen, hvor CAS-operationen ikke fungerede. Vi modellerer bare koden, så den ikke går videre, før operationen lykkes.
Introduktion til atomtyper
Er du stødt på en situation, hvor du skal konfigurere synkronisering for den enkleste variabel af typen int ?
Den første måde, vi allerede har dækket, er at bruge volatile + synchronized . Men der er også særlige Atomic* klasser.
Hvis vi bruger CAS, så fungerer operationer hurtigere sammenlignet med den første metode. Og derudover har vi specielle og meget praktiske metoder til at tilføje en værdi og øge og formindske operationer.
AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray er klasser, hvor operationer er atomare. Nedenfor vil vi analysere arbejdet med dem.
AtomicInteger
AtomicInteger- klassen leverer operationer på en int- værdi , der kan læses og skrives atomisk, ud over at give udvidede atomoperationer.
Det har få og indstille metoder , der fungerer som læse- og skrivevariable.
Det vil sige "sker-før" med enhver efterfølgende modtagelse af den samme variabel, som vi talte om tidligere. Atomic compareAndSet- metoden har også disse hukommelseskonsistensfunktioner.
Alle operationer, der returnerer en ny værdi, udføres atomisk:
int addAndGet (int delta) | Tilføjer en bestemt værdi til den aktuelle værdi. |
boolean compareAndSet(forventet int, update int) | Indstiller værdien til den givne opdaterede værdi, hvis den aktuelle værdi matcher den forventede værdi. |
int decrementAndGet() | Formindsker den aktuelle værdi med én. |
int getAndAdd(int delta) | Tilføjer den givne værdi til den aktuelle værdi. |
int getAndDecrement() | Formindsker den aktuelle værdi med én. |
int getAndIncrement() | Øger den aktuelle værdi med én. |
int getAndSet(int newValue) | Indstiller den givne værdi og returnerer den gamle værdi. |
int incrementAndGet() | Øger den aktuelle værdi med én. |
lazySet(int newValue) | Indstil endelig til den givne værdi. |
boolean weakCompareAndSet(forventet, opdatering int) | Indstiller værdien til den givne opdaterede værdi, hvis den aktuelle værdi matcher den forventede værdi. |
Eksempel:
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