Inleiding tot het Java-geheugenmodel

Het Java Memory Model (JMM) beschrijft het gedrag van threads in de Java-runtime-omgeving. Het geheugenmodel maakt deel uit van de semantiek van de Java-taal en beschrijft wat een programmeur wel en niet mag verwachten bij het ontwikkelen van software, niet voor een specifieke Java-machine, maar voor Java als geheel.

Het originele Java-geheugenmodel (dat in het bijzonder verwijst naar "percolocal memory"), ontwikkeld in 1995, wordt als een mislukking beschouwd: veel optimalisaties kunnen niet worden gemaakt zonder de garantie van codeveiligheid te verliezen. Er zijn met name verschillende opties om multi-threaded "single" te schrijven:

  • ofwel zal elke handeling van toegang tot een singleton (zelfs als het object lang geleden is gemaakt en er niets kan veranderen) een inter-thread lock veroorzaken;
  • of onder bepaalde omstandigheden zal het systeem een ​​onvoltooide eenling afgeven;
  • of onder bepaalde omstandigheden zal het systeem twee eenlingen creëren;
  • of het ontwerp zal afhangen van het gedrag van een bepaalde machine.

Daarom is het geheugenmechanisme opnieuw ontworpen. In 2005, met de release van Java 5, werd een nieuwe aanpak gepresenteerd, die verder werd verbeterd met de release van Java 14.

Het nieuwe model is gebaseerd op drie regels:

Regel #1 : Programma's met één thread worden pseudo-sequentieel uitgevoerd. Dit betekent: in werkelijkheid kan de processor meerdere bewerkingen per klok uitvoeren en tegelijkertijd hun volgorde wijzigen, maar alle gegevensafhankelijkheden blijven bestaan, dus het gedrag verschilt niet van sequentieel.

Regel nummer 2 : er zijn geen waarden die uit het niets komen. Het lezen van een variabele (behalve niet-vluchtige long en double, waarvoor deze regel mogelijk niet geldt) zal ofwel de standaardwaarde (nul) retourneren of iets dat daar door een ander commando is geschreven.

En regel nummer 3 : de rest van de gebeurtenissen worden op volgorde uitgevoerd, als ze zijn verbonden door een strikte gedeeltelijke volgorderelatie "wordt uitgevoerd voordat" ( gebeurt voordat ).

Gebeurt eerder

Leslie Lamport bedacht eerder het concept van Happens . Dit is een strikte partiële orde-relatie die is geïntroduceerd tussen atomaire commando's (++ en -- zijn niet atomair) en betekent niet "fysiek ervoor".

Er staat dat het tweede team "op de hoogte zal zijn" van de wijzigingen die door het eerste zijn aangebracht.

Gebeurt eerder

De ene wordt bijvoorbeeld vóór de andere uitgevoerd voor dergelijke bewerkingen:

Synchronisatie en monitoren:

  • De monitor vastleggen ( vergrendelingsmethode , gesynchroniseerde start) en wat er daarna op dezelfde thread gebeurt.
  • De terugkeer van de monitor (ontgrendelingsmethode , einde van gesynchroniseerd) en wat er ook gebeurt op dezelfde thread ervoor.
  • De monitor retourneren en vervolgens vastleggen via een andere thread.

Schrijven en lezen:

  • Schrijven naar een willekeurige variabele en deze vervolgens lezen in dezelfde stroom.
  • Alles in dezelfde thread voordat u naar de vluchtige variabele schrijft, en het schrijven zelf. vluchtig lezen en alles op dezelfde draad erna.
  • Schrijven naar een vluchtige variabele en vervolgens opnieuw lezen. Een vluchtige schrijfactie werkt op dezelfde manier samen met het geheugen als een monitorretour, terwijl een leesactie als een opname is. Het blijkt dat als de ene thread naar een vluchtige variabele schreef en de tweede deze vond, alles dat aan het schrijven voorafgaat, wordt uitgevoerd vóór alles wat na het lezen komt; zie foto.

Objectonderhoud:

  • Statische initialisatie en alle acties met alle instanties van objecten.
  • Schrijven naar laatste velden in de constructor en alles na de constructor. Bij wijze van uitzondering maakt de voor-gebeurtenis-relatie geen overgangsverbinding met andere regels en kan daarom een ​​race tussen threads veroorzaken.
  • Elk werk met het object en finalize() .

Stream-service:

  • Een thread starten en eventuele code in de thread.
  • Variabelen op nul zetten die betrekking hebben op de thread en eventuele code in de thread.
  • Code in thread en join() ; code in de thread en isAlive() == false .
  • interrupt() de thread en detecteer dat deze is gestopt.

Gebeurt vóór werknuances

Het vrijgeven van een happen-before-monitor vindt plaats voordat dezelfde monitor wordt aangeschaft. Het is vermeldenswaard dat het de release is, en niet de exit, dat wil zeggen dat u zich geen zorgen hoeft te maken over de veiligheid wanneer u wacht gebruikt.

Laten we eens kijken hoe deze kennis ons zal helpen ons voorbeeld te corrigeren. In dit geval is alles heel eenvoudig: verwijder gewoon de externe controle en laat de synchronisatie zoals die is. Nu ziet de tweede thread gegarandeerd alle wijzigingen, omdat deze de monitor pas krijgt nadat de andere thread deze heeft vrijgegeven. En aangezien hij het pas zal vrijgeven als alles is geïnitialiseerd, zullen we alle wijzigingen in één keer zien, en niet afzonderlijk:

public class Keeper {
    private Data data = null;

    public Data getData() {
        synchronized(this) {
            if(data == null) {
                data = new Data();
            }
        }

        return data;
    }
}

Het schrijven naar een vluchtige variabele gebeurt vóór het lezen van dezelfde variabele. De wijziging die we hebben aangebracht, lost natuurlijk de bug op, maar het zet degene die de originele code heeft geschreven terug waar het vandaan kwam - elke keer blokkeren. Het vluchtige zoekwoord kan besparen. In feite betekent de betreffende verklaring dat we bij het lezen van alles dat vluchtig wordt verklaard, altijd de werkelijke waarde krijgen.

Bovendien, zoals ik al eerder zei, is schrijven voor vluchtige velden altijd (ook lang en dubbel) een atomaire operatie. Nog een belangrijk punt: als je een vluchtige entiteit hebt die verwijzingen naar andere entiteiten heeft (bijvoorbeeld een array, List of een andere klasse), dan zal alleen een verwijzing naar de entiteit zelf altijd "nieuw" zijn, maar niet naar alles in het inkomend.

Dus, terug naar onze dubbelsluitende rammen. Met vluchtig kunt u de situatie als volgt oplossen:

public class Keeper {
    private volatile Data data = null;

    public Data getData() {
        if(data == null) {
            synchronized(this) {
                if(data == null) {
                    data = new Data();
                }
            }
        }
        return data;
    }
}

Hier hebben we nog steeds een slot, maar alleen als data == null. We filteren de resterende gevallen eruit met behulp van vluchtig lezen. Correctheid wordt verzekerd door het feit dat vluchtige opslag plaatsvindt vóór vluchtige lezing, en alle bewerkingen die plaatsvinden in de constructor zijn zichtbaar voor iedereen die de waarde van het veld leest.