Atomaarsed muutujad
Sissejuhatus
synchronized sobib igasuguste liitoperatsioonide jaoks, kuid on suhteliselt nõudlik lahendus:
kui lõim jõuab lukustatud koodini, tuleb see peatada, hiljem uuesti äratada ja operatsioonisüsteemi poolt uuesti ajastada.
Kui näiteks ühte loendurit uuendatakse miljon korda sekundis paljude lõimede poolt, koguneb see lisakulu märgatavaks.
Tänapäevased protsessorid pakuvad spetsiaalset käsku - compare-and-swap -, mis võimaldab lihtsaid operatsioone, nagu arvu suurendamine, teha atomaarseks ilma lukku kasutamata.
Java pakub selleks erinevaid vahendeid, mis asuvad java.util.concurrent.atomic pakis.
AtomicInteger
AtomicInteger kapseldab int-tüüpi välja ja pakub sellel atomaarseid operatsioone:
import java.util.concurrent.atomic.AtomicInteger;
public class RequestStats {
private final AtomicInteger total = new AtomicInteger(0);
private final AtomicInteger ok = new AtomicInteger(0);
private final AtomicInteger errors = new AtomicInteger(0);
public void recordOk() {
total.incrementAndGet();
ok.incrementAndGet();
}
public void recordError() {
total.incrementAndGet();
errors.incrementAndGet();
}
public int getTotal() { return total.get(); }
public int getOk() { return ok.get(); }
public int getErrors() { return errors.get(); }
}
incrementAndGet on ++-i atomaarne vaste.
Mitu lõime saavad sama AtomicInteger-i samaaegselt kasutada ilma konfliktideta, ilma lukke kasutamata ja üksteist blokeerimata.
Levinumad operatsioonid:
| Meetod | Tagastus | Kirjeldus |
|---|---|---|
get() | int | Hetkeväärtus |
set(v) | void | Asendab väärtuse |
incrementAndGet() | int | ++value |
decrementAndGet() | int | --value |
getAndIncrement() | int | value++ |
addAndGet(n) | int | value += n; return value |
compareAndSet(expected, new) | boolean | Kui praegune väärtus on expected, muudetakse see väärtuseks new |
Kuidas compare-and-swap töötab
AtomicInteger ei kasuta lukke.
See kasutab protsessori ühte atomaarset käsku, mis kontrollib ja muudab väärtust järgmisel põhimõttel: kui praegune väärtus vastab oodatule, asendatakse see uuega.
Kui mõni teine lõim on vahepeal väärtust muutnud, operatsioon ebaõnnestub ning lõim proovib seda uuesti.
Lihtsustatud pseudokoodis näeb incrementAndGet välja nii:
int incrementAndGet() {
while (true) {
int current = value;
int next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
// another thread changed value - retry
}
}
Koormuse all võib see tsükkel mõned korrad korduda, muul ajal õnnestub operatsioon esimesel katsel. Mõlemal juhul ei panda ühtegi lõime kunagi lukku ootama.
Atomaarne vs sünkroniseeritud
Atomaarsed muutujad on tavaliselt kiiremad kui synchronized märksõna kasutamine, kui tegemist on lihtsate operatsioonidega ühe välja peal.
Kuid need kaitsevad ainult ühte väärtust korraga.
Kui on vaja mitme välja samaaegset uuendamist, on õigeks tööriistaks kas synchronized või Lock kasutamine:
// Two fields that must move together - synchronized is required
public class Range {
private int min;
private int max;
public synchronized void shift(int delta) {
min += delta;
max += delta;
}
}
Siin ei aitaks int-i asendamine AtomicInteger-iga.
Üks lõim võib näha uuendatud min-väärtust kuid max-i puhul vana väärtust.
Teised atomaarsed tüübid
Lisaks AtomicInteger klassile sisaldab java.util.concurrent.atomic pakk:
AtomicLong-long-tüübi jaoks, samad operatsioonidAtomicBoolean- kasulik ühekordsete lippude jaoksAtomicReference<T>- atomaarne viide suvalisele objektile, kooscompareAndSet-igaAtomicIntegerArray,AtomicLongArray,AtomicReferenceArray<T>- atomaaroperatsioonid massiivide üksikute elementide jaoksLongAdder- suure koormuse all töötav loendur, mis hajutab uuendused mitme sisemise väärtuse vahel; kiirem kuiAtomicLong, kuidsum()ei anna täpselt atomaarset hetkeväärtust
AtomicReference on eriti kasulik muutumatute objektide lukuvabaks vahetamiseks:
AtomicReference<Configuration> config = new AtomicReference<>(initial);
// hot reload from another thread - the swap is atomic
config.set(loadFromDisk());
// readers always see a complete configuration
Configuration current = config.get();
Kokkuvõtteks
Loendurite puhul on AtomicInteger peaaegu alati õige valik.
Kuid see ei asenda synchronized-i täies mahus.
Põhjuseks on:
- Mitut välja hõlmavad invariandid vajavad lukku
- Operatsioonid, mille järjekord peab olema jälgitav, vajavad lukku
- Operatsioonid, mis sõltuvad muust olekust, vajavad lukku
Atomaarmuutujad sobivad eelkõige ühe väärtuse koordineerimiseks: loendurid, lipud ja lihtsad viited, mida vahetatakse atomaarselt.