Gids voor het vluchtige trefwoord in Java

1. Overzicht

Bij gebrek aan noodzakelijke synchronisaties kunnen de compiler, runtime of processors allerlei soorten optimalisaties toepassen. Hoewel deze optimalisaties meestal gunstig zijn, kunnen ze soms subtiele problemen veroorzaken.

Caching en herschikking zijn enkele van de optimalisaties die ons in gelijktijdige contexten kunnen verrassen. Java en de JVM bieden veel manieren om de geheugenvolgorde te bepalen, en de vluchtig trefwoord is er een van.

In dit artikel zullen we ons concentreren op dit fundamentele maar vaak verkeerd begrepen concept in de Java-taal - het vluchtig trefwoord. Eerst beginnen we met wat achtergrondinformatie over hoe de onderliggende computerarchitectuur werkt, en daarna zullen we vertrouwd raken met de geheugenvolgorde in Java.

2. Gedeelde multiprocessorarchitectuur

Verwerkers zijn verantwoordelijk voor het uitvoeren van programma-instructies. Daarom moeten ze zowel programma-instructies als vereiste gegevens uit RAM ophalen.

Omdat CPU's in staat zijn om een ​​aanzienlijk aantal instructies per seconde uit te voeren, is ophalen uit RAM niet zo ideaal voor hen. Om deze situatie te verbeteren, gebruiken verwerkers trucs zoals Out of Order Execution, Branch Prediction, Speculative Execution en, natuurlijk, Caching.

Dit is waar de volgende geheugenhiërarchie in het spel komt:

Naarmate verschillende kernen meer instructies uitvoeren en meer gegevens manipuleren, vullen ze hun caches met relevantere gegevens en instructies. Dit zal de algehele prestaties verbeteren ten koste van de introductie van cache-coherentie-uitdagingen.

Simpel gezegd, we moeten twee keer nadenken over wat er gebeurt als een thread een waarde in de cache bijwerkt.

3. Wanneer te gebruiken vluchtig

Laten we, om meer uit te breiden over de cache-coherentie, een voorbeeld lenen uit het boek Java Concurrency in Practice:

openbare klasse TaskRunner {privé statisch int nummer; privé statische boolean gereed; privé statische klasse Reader breidt Thread uit {@Override public void run () {while (! ready) {Thread.yield (); } System.out.println (nummer); }} public static void main (String [] args) {nieuwe Reader (). start (); getal = 42; klaar = waar; }}

De TaskRunner klasse onderhoudt twee eenvoudige variabelen. In zijn belangrijkste methode creëert het een andere draad die op de klaar variabel zolang het is false. Wanneer de variabele wordt waar, de draad zal gewoon de aantal variabele.

Velen zullen verwachten dat dit programma na een korte vertraging gewoon 42 afdrukt. In werkelijkheid kan de vertraging echter veel langer zijn. Het kan zelfs voor altijd blijven hangen, of zelfs nul afdrukken!

De oorzaak van deze anomalieën is het gebrek aan goede zichtbaarheid van het geheugen en het opnieuw ordenen. Laten we ze in meer detail evalueren.

3.1. Geheugenzichtbaarheid

In dit eenvoudige voorbeeld hebben we twee toepassingsthreads: de hoofdthread en de readerthread. Laten we ons een scenario voorstellen waarin het besturingssysteem die threads plant op twee verschillende CPU-kernen, waarbij:

  • De hoofdthread heeft een kopie van klaar en aantal variabelen in de kerncache
  • De thread van de lezer eindigt ook met zijn kopieën
  • De hoofdthread werkt de waarden in de cache bij

Op de meeste moderne processors worden schrijfverzoeken niet meteen toegepast nadat ze zijn uitgegeven. In feite, processors hebben de neiging om die schrijfbewerkingen in een speciale schrijfbuffer te plaatsen. Na een tijdje passen ze die schrijfbewerkingen allemaal tegelijk toe op het hoofdgeheugen.

Met dat alles gezegd, wanneer de hoofdthread het aantal en klaar variabelen, is er geen garantie over wat de thread van de lezer kan zien. Met andere woorden, de thread van de lezer kan de bijgewerkte waarde meteen zien, of met enige vertraging, of helemaal nooit!

Deze zichtbaarheid van het geheugen kan levensproblemen veroorzaken in programma's die afhankelijk zijn van zichtbaarheid.

3.2. Herordenen

Om het nog erger te maken, de thread van de lezer kan die schrijfacties in elke andere volgorde zien dan de feitelijke programmavolgorde. Omdat we bijvoorbeeld het aantal variabele:

public static void main (String [] args) {nieuwe Reader (). start (); getal = 42; klaar = waar; }

We mogen de threadprints 42 van de lezer verwachten. Het is echter mogelijk om nul als afgedrukte waarde te zien!

De herschikking is een optimalisatietechniek voor prestatieverbeteringen. Interessant is dat verschillende componenten deze optimalisatie kunnen toepassen:

  • De processor kan zijn schrijfbuffer leegmaken in elke andere volgorde dan de programmavolgorde
  • De processor kan een uitvoeringstechniek buiten de bestelling toepassen
  • De JIT-compiler kan optimaliseren via herschikking

3.3. vluchtig Geheugenvolgorde

Om ervoor te zorgen dat updates van variabelen voorspelbaar worden doorgegeven aan andere threads, moeten we de vluchtig modifier voor die variabelen:

openbare klasse TaskRunner {privé vluchtig statisch int nummer; privé vluchtige statische boolean gereed; // hetzelfde als voorheen }

Op deze manier communiceren we met runtime en processor om geen instructies met betrekking tot de vluchtig variabele. Verwerkers begrijpen ook dat ze eventuele updates van deze variabelen meteen moeten doorspoelen.

4. vluchtig en draadsynchronisatie

Voor toepassingen met meerdere threads moeten we zorgen voor een aantal regels voor consistent gedrag:

  • Wederzijdse uitsluiting - slechts één thread voert een kritieke sectie tegelijk uit
  • Zichtbaarheid - wijzigingen die door een thread zijn aangebracht in de gedeelde gegevens, zijn zichtbaar voor andere threads om de gegevensconsistentie te behouden

gesynchroniseerd methoden en blokken bieden beide bovenstaande eigenschappen, ten koste van de prestaties van de applicatie.

vluchtig is best een handig trefwoord omdat het kan helpen het zichtbaarheidsaspect van de gegevenswijziging te waarborgen zonder uiteraard wederzijdse uitsluiting te bieden. Het is dus handig op de plaatsen waar het goed is met meerdere threads die een codeblok parallel uitvoeren, maar we moeten de zichtbaarheidseigenschap garanderen.

5. Gebeurt - voordat u bestelt

De geheugenzichtbaarheidseffecten van vluchtig variabelen reiken verder dan de vluchtig variabelen zelf.

Laten we, om de zaken concreter te maken, aannemen dat thread A naar een vluchtig variabele, en thread B leest hetzelfde vluchtig variabele. In dergelijke gevallen, de waarden die zichtbaar waren voor A voordat het vluchtig variabele zal zichtbaar zijn voor B na het lezen van de vluchtig variabele:

Technisch gezien kan elk schrijven naar een vluchtig veld gebeurt vóór elke volgende lezing van hetzelfde veld. Dit is de vluchtig variabele regel van het Java Memory Model (JMM).

5.1. Meeliften

Vanwege de kracht van de 'happen-before'-geheugenvolgorde, kunnen we soms meeliften op de zichtbaarheidseigenschappen van een ander vluchtig variabele. In ons specifieke voorbeeld hoeven we bijvoorbeeld alleen de klaar variabele als vluchtig:

openbare klasse TaskRunner {privé statisch int nummer; // niet vluchtige privé vluchtige statische boolean gereed; // hetzelfde als voorheen }

Alles voorafgaand aan het schrijven waar naar de klaar variabele is voor alles zichtbaar na het lezen van de klaar variabele. Daarom, de aantal variabele meeliften op de zichtbaarheid van het geheugen afgedwongen door de klaar variabele. Simpel gezegd, ook al is het geen vluchtig variabele, het vertoont een vluchtig gedrag.

Door gebruik te maken van deze semantiek, kunnen we slechts enkele van de variabelen in onze klasse definiëren als vluchtig en optimaliseer de zichtbaarheid garantie.

6. Conclusie

In deze zelfstudie hebben we meer over de vluchtig trefwoord en de mogelijkheden ervan, evenals de verbeteringen die eraan zijn aangebracht te beginnen met Java 5.

Zoals altijd zijn de codevoorbeelden te vinden op GitHub.


$config[zx-auto] not found$config[zx-overlay] not found