Minnes hårdvaruarkitektur
Modern minneshårdvaruarkitektur skiljer sig från Javas interna minnesmodell. Därför måste du förstå hårdvaruarkitekturen för att veta hur Java-modellen fungerar med den. Det här avsnittet beskriver den allmänna minneshårdvaruarkitekturen och nästa avsnitt beskriver hur Java fungerar med den.
Här är ett förenklat diagram över hårdvaruarkitekturen för en modern dator:
I den moderna världen har en dator 2 eller fler processorer och detta är redan normen. Vissa av dessa processorer kan också ha flera kärnor. På sådana datorer är det möjligt att köra flera trådar samtidigt. Varje processorkärna är kapabel att exekvera en tråd vid varje given tidpunkt. Detta innebär att alla Java-applikationer är a priori flertrådade, och inom ditt program kan en tråd per processorkärna köras åt gången.
Processorns kärna innehåller en uppsättning register som finns i dess minne (inuti kärnan). Den utför operationer på registerdata mycket snabbare än på data som finns i datorns huvudminne (RAM). Detta beror på att processorn kan komma åt dessa register mycket snabbare.
Varje CPU kan också ha sitt eget cachelager. De flesta moderna processorer har det. Processorn kan komma åt sin cache mycket snabbare än huvudminnet, men inte lika snabbt som dess interna register. Värdet på cacheåtkomsthastigheten ligger ungefär mellan åtkomsthastigheterna för huvudminnet och interna register.
Dessutom har processorer en plats att ha en multi-level cache. Men detta är inte så viktigt att veta för att förstå hur Java-minnesmodellen interagerar med hårdvaruminne. Det är viktigt att veta att processorer kan ha en viss nivå av cache.
Alla datorer innehåller också RAM (huvudminnesområdet) på samma sätt. Alla kärnor kan komma åt huvudminnet. Huvudminnesområdet är vanligtvis mycket större än cacheminnet för processorkärnorna.
I det ögonblick då processorn behöver tillgång till huvudminnet läser den in en del av det i sitt cacheminne. Den kan också läsa en del data från cachen till sina interna register och sedan utföra operationer på dem. När processorn behöver skriva tillbaka resultatet till huvudminnet kommer den att spola data från sitt interna register till cache och vid något tillfälle till huvudminnet.
Data som lagras i cachen spolas normalt tillbaka till huvudminnet när processorn behöver lagra något annat i cachen. Cachen har förmågan att rensa sitt minne och skriva data samtidigt. Processorn behöver inte läsa eller skriva hela cachen varje gång under en uppdatering. Vanligtvis uppdateras cachen i små minnesblock, de kallas "cache-linje". En eller flera "cache-rader" kan läsas in i cacheminnet, och en eller flera cache-rader kan spolas tillbaka till huvudminnet.
Kombinera Java-minnesmodell och minneshårdvaruarkitektur
Som redan nämnts är Java-minnesmodellen och minneshårdvaruarkitekturen olika. Hårdvaruarkitekturen skiljer inte mellan trådstaplar och heaps. På hårdvara finns trådstacken och HEAP (heap) i huvudminnet.
Delar av stackar och trådhögar kan ibland finnas i cacher och interna register hos CPU:n. Detta visas i diagrammet:
När objekt och variabler kan lagras i olika delar av datorns minne kan vissa problem uppstå. Här är de två huvudsakliga:
- Synlighet av ändringarna som tråden har gjort i delade variabler.
- Tävlingsförhållande vid läsning, kontroll och skrivning av delade variabler.
Båda dessa frågor kommer att förklaras nedan.
Synlighet för delade objekt
Om två eller flera trådar delar ett objekt utan korrekt användning av flyktig deklaration eller synkronisering, kanske ändringar av det delade objektet som gjorts av en tråd inte är synliga för andra trådar.
Föreställ dig att ett delat objekt initialt lagras i huvudminnet. En tråd som körs på en CPU läser det delade objektet in i cachen för samma CPU. Där gör han ändringar i föremålet. Tills processorns cache har tömts till huvudminnet är den modifierade versionen av det delade objektet inte synlig för trådar som körs på andra processorer. Således kan varje tråd få sin egen kopia av det delade objektet, varje kopia kommer att finnas i en separat CPU-cache.
Följande diagram illustrerar en översikt över denna situation. En tråd som körs på den vänstra CPU:n kopierar det delade objektet till sin cache och ändrar värdet på count till 2. Denna ändring är osynlig för andra trådar som körs på den högra CPU:n eftersom uppdateringen för att räkna ännu inte har spolas tillbaka till huvudminnet.
För att lösa detta problem kan du använda nyckelordet volatile när du deklarerar en variabel. Den kan säkerställa att en given variabel läses direkt från huvudminnet och alltid skrivs tillbaka till huvudminnet när den uppdateras.
Race skick
Om två eller flera trådar delar samma objekt och mer än en tråd uppdaterar variabler i det delade objektet, kan ett race-tillstånd uppstå.
Föreställ dig att tråd A läser in det delade objektets räkningsvariabel i dess processors cache. Föreställ dig också att tråd B gör samma sak, men i en annan processors cache. Nu lägger tråd A till 1 till värdet på count, och tråd B gör detsamma. Nu har variabeln ökats två gånger - separat med +1 i cachen för varje processor.
Om dessa inkrement utfördes sekventiellt skulle räknevariabeln fördubblas och skrivas tillbaka till huvudminnet (ursprungligt värde + 2).
Men två steg utfördes samtidigt utan korrekt synkronisering. Oavsett vilken tråd (A eller B) som skriver sin uppdaterade version av count till huvudminnet, kommer det nya värdet bara att vara 1 mer än det ursprungliga värdet, trots de två stegen.
Detta diagram illustrerar förekomsten av tävlingsproblemet som beskrivs ovan:
För att lösa detta problem kan du använda Java-synkroniserat block. Ett synkroniserat block säkerställer att endast en tråd kan komma in i en given kritisk del av koden vid varje given tidpunkt.
Synkroniserade block garanterar också att alla variabler som nås inuti det synkroniserade blocket kommer att läsas från huvudminnet, och när tråden lämnar det synkroniserade blocket kommer alla uppdaterade variabler att spolas tillbaka till huvudminnet, oavsett om variabeln är deklarerad flyktig eller nej.
GO TO FULL VERSION