Liigu peamise sisu juurde

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:

MeetodTagastusKirjeldus
get()intHetkeväärtus
set(v)voidAsendab väärtuse
incrementAndGet()int++value
decrementAndGet()int--value
getAndIncrement()intvalue++
addAndGet(n)intvalue += n; return value
compareAndSet(expected, new)booleanKui 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 operatsioonid
  • AtomicBoolean - kasulik ühekordsete lippude jaoks
  • AtomicReference<T> - atomaarne viide suvalisele objektile, koos compareAndSet-iga
  • AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray<T> - atomaaroperatsioonid massiivide üksikute elementide jaoks
  • LongAdder - suure koormuse all töötav loendur, mis hajutab uuendused mitme sisemise väärtuse vahel; kiirem kui AtomicLong, kuid sum() 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.