Een inleiding tot atomaire variabelen in Java

1. Inleiding

Simpel gezegd, een gedeelde veranderlijke toestand leidt heel gemakkelijk tot problemen wanneer er sprake is van gelijktijdigheid. Als toegang tot gedeelde veranderlijke objecten niet correct wordt beheerd, kunnen applicaties snel vatbaar worden voor enkele moeilijk te detecteren gelijktijdigheidsfouten.

In dit artikel zullen we het gebruik van vergrendelingen om gelijktijdige toegang af te handelen opnieuw bekijken, enkele van de nadelen van vergrendelingen onderzoeken en ten slotte atoomvariabelen als alternatief introduceren.

2. Sloten

Laten we de klas eens bekijken:

openbare klasse Counter {int counter; public void increment () {counter ++; }}

In het geval van een single-threaded omgeving werkt dit perfect; Zodra we echter meer dan één thread toestaan ​​om te schrijven, beginnen we inconsistente resultaten te krijgen.

Dit komt door de eenvoudige ophogingsbewerking (teller ++), die er misschien uitziet als een atomaire bewerking, maar in feite een combinatie is van drie bewerkingen: de waarde ophalen, verhogen en de bijgewerkte waarde terugschrijven.

Als twee threads de waarde tegelijkertijd proberen op te halen en bij te werken, kan dit resulteren in verloren updates.

Een van de manieren om de toegang tot een object te beheren, is door vergrendelingen te gebruiken. Dit kan worden bereikt door de gesynchroniseerd trefwoord in het toename methode handtekening. De gesynchroniseerd trefwoord zorgt ervoor dat slechts één thread tegelijk de methode kan openen (voor meer informatie over vergrendelen en synchroniseren, zie - Gids voor gesynchroniseerd trefwoord in Java):

openbare klasse SafeCounterWithLock {privé vluchtige int teller; openbare gesynchroniseerde ongeldige increment () {counter ++; }}

Bovendien moeten we het vluchtig trefwoord om een ​​goede zichtbaarheid van verwijzingen tussen threads te garanderen.

Het gebruik van sloten lost het probleem op. De uitvoering krijgt echter een hit.

Wanneer meerdere threads proberen een lock te krijgen, wint een van hen, terwijl de rest van de threads wordt geblokkeerd of opgeschort.

Het proces van het opschorten en vervolgens hervatten van een thread is erg duur en beïnvloedt de algehele efficiëntie van het systeem.

In een klein programma, zoals de tellerkan de tijd die wordt besteed aan het wisselen van context veel meer worden dan de daadwerkelijke uitvoering van code, waardoor de algehele efficiëntie aanzienlijk wordt verminderd.

3. Atoomoperaties

Er is een tak van onderzoek gericht op het maken van niet-blokkerende algoritmen voor gelijktijdige omgevingen. Deze algoritmen maken gebruik van atomaire machine-instructies op laag niveau, zoals vergelijk-en-ruil (CAS), om de gegevensintegriteit te waarborgen.

Een typische CAS-bewerking werkt op drie operanden:

  1. De geheugenlocatie waarop moet worden gewerkt (M)
  2. De bestaande verwachte waarde (A) van de variabele
  3. De nieuwe waarde (B) die moet worden ingesteld

De CAS-bewerking werkt atomair de waarde in M ​​bij naar B, maar alleen als de bestaande waarde in M ​​overeenkomt met A, anders wordt er geen actie ondernomen.

In beide gevallen wordt de bestaande waarde in M ​​geretourneerd. Dit combineert drie stappen - de waarde ophalen, de waarde vergelijken en de waarde bijwerken - in één bewerking op machineniveau.

Wanneer meerdere threads dezelfde waarde proberen bij te werken via CAS, wint een van hen en werkt de waarde bij. In tegenstelling tot bij sloten blijft er echter geen andere draad hangen; in plaats daarvan worden ze gewoon geïnformeerd dat ze er niet in zijn geslaagd de waarde bij te werken. De threads kunnen dan verder werken en contextschakelaars worden volledig vermeden.

Een ander gevolg is dat de kernprogrammalogica complexer wordt. Dit komt omdat we het scenario moeten afhandelen wanneer de CAS-bewerking niet is gelukt. We kunnen het steeds opnieuw proberen totdat het lukt, of we kunnen niets doen en verder gaan, afhankelijk van het gebruik.

4. Atomaire variabelen in Java

De meest gebruikte atomaire variabeleklassen in Java zijn AtomicInteger, AtomicLong, AtomicBoolean en AtomicReference. Deze klassen vertegenwoordigen een int, lang, boolean, en objectreferentie die atomair kunnen worden bijgewerkt. De belangrijkste methoden die door deze klassen worden blootgelegd, zijn:

  • krijgen() - haalt de waarde uit het geheugen, zodat wijzigingen die door andere threads zijn aangebracht, zichtbaar zijn; gelijk aan het lezen van een vluchtig variabele
  • set () - schrijft de waarde naar het geheugen, zodat de wijziging zichtbaar is voor andere threads; gelijk aan het schrijven van een vluchtig variabele
  • lazySet () - schrijft de waarde uiteindelijk naar het geheugen, mogelijk opnieuw gerangschikt met daaropvolgende relevante geheugenbewerkingen. Een use-case is het tenietdoen van referenties, omwille van garbage collection, die nooit meer zal worden geopend. In dit geval worden betere prestaties bereikt door de nul te vertragen vluchtig schrijven
  • vergelijkAndSet () - hetzelfde als beschreven in sectie 3, geeft true terug als het lukt, anders false
  • zwakkeCompareAndSet () - hetzelfde als beschreven in sectie 3, maar zwakker in die zin dat het geen happen-before-ordeningen creëert. Dit betekent dat het mogelijk niet noodzakelijkerwijs updates van andere variabelen ziet. Vanaf Java 9 is deze methode verouderd in alle atomaire implementaties ten gunste van zwakkeCompareAndSetPlain (). De geheugeneffecten van zwakkeCompareAndSet () waren duidelijk, maar de namen impliceerden vluchtige geheugeneffecten. Om deze verwarring te voorkomen, hebben ze deze methode afgekeurd en vier methoden met verschillende geheugeneffecten toegevoegd, zoals zwakkeCompareAndSetPlain () of zwakCompareAndSetVolatile ()

Een draadveilige teller geïmplementeerd met AtomicInteger wordt getoond in het onderstaande voorbeeld:

openbare klasse SafeCounterWithoutLock {privé finale AtomicInteger teller = nieuwe AtomicInteger (0); public int getValue () {return counter.get (); } public void increment () {while (true) {int bestaandeValue = getValue (); int newValue = bestaandeValue + 1; if (counter.compareAndSet (bestaandeValue, newValue)) {return; }}}}

Zoals u kunt zien, proberen we het vergelijkAndSet werking en opnieuw op falen, aangezien we willen garanderen dat de oproep naar de toename methode verhoogt altijd de waarde met 1.

5. Conclusie

In deze korte tutorial hebben we een alternatieve manier beschreven om gelijktijdigheid te behandelen waarbij de nadelen die verband houden met vergrendeling kunnen worden vermeden. We hebben ook gekeken naar de belangrijkste methoden die worden blootgelegd door de atomaire variabeleklassen in Java.

Zoals altijd zijn de voorbeelden allemaal beschikbaar op GitHub.

Om meer klassen te verkennen die intern niet-blokkerende algoritmen gebruiken, raadpleegt u een gids voor ConcurrentMap.