CodeGym/Java Course/Modulo 3/Memoria nella JVM, parte 2

Memoria nella JVM, parte 2

Disponibile

Architettura hardware della memoria

La moderna architettura hardware della memoria differisce dal modello di memoria interna di Java. Pertanto, è necessario comprendere l'architettura hardware per sapere come funziona il modello Java con essa. Questa sezione descrive l'architettura hardware della memoria generale e la sezione successiva descrive come funziona Java con essa.

Ecco uno schema semplificato dell'architettura hardware di un computer moderno:

Architettura hardware della memoria

Nel mondo moderno, un computer ha 2 o più processori e questa è già la norma. Alcuni di questi processori possono anche avere più core. Su tali computer è possibile eseguire più thread contemporaneamente. Ogni core del processore è in grado di eseguire un thread in qualsiasi momento. Ciò significa che qualsiasi applicazione Java è a priori multi-thread e all'interno del programma può essere eseguito un thread per core del processore alla volta.

Il core del processore contiene una serie di registri che risiedono nella sua memoria (all'interno del core). Esegue operazioni sui dati del registro molto più velocemente rispetto ai dati che risiedono nella memoria principale del computer (RAM). Questo perché il processore può accedere a questi registri molto più velocemente.

Ogni CPU può anche avere il proprio livello di cache. La maggior parte dei processori moderni ce l'ha. Il processore può accedere alla sua cache molto più velocemente della memoria principale, ma non quanto i suoi registri interni. Il valore della velocità di accesso alla cache è approssimativamente compreso tra le velocità di accesso della memoria principale e dei registri interni.

Inoltre, i processori hanno un posto dove avere una cache multilivello. Ma questo non è così importante da sapere per capire come il modello di memoria Java interagisce con la memoria hardware. È importante sapere che i processori possono avere un certo livello di cache.

Qualsiasi computer contiene anche RAM (area di memoria principale) allo stesso modo. Tutti i core possono accedere alla memoria principale. L'area di memoria principale è solitamente molto più grande della memoria cache dei core del processore.

Nel momento in cui il processore ha bisogno di accedere alla memoria principale, ne legge una parte nella sua memoria cache. Può anche leggere alcuni dati dalla cache nei suoi registri interni e quindi eseguire operazioni su di essi. Quando la CPU deve riscrivere il risultato nella memoria principale, scarica i dati dal suo registro interno nella cache e, a un certo punto, nella memoria principale.

I dati archiviati nella cache vengono normalmente scaricati nella memoria principale quando il processore deve memorizzare qualcos'altro nella cache. La cache ha la capacità di cancellare la sua memoria e scrivere dati allo stesso tempo. Il processore non ha bisogno di leggere o scrivere l'intera cache ogni volta durante un aggiornamento. Solitamente la cache viene aggiornata in piccoli blocchi di memoria, si chiamano "cache line". Una o più "righe cache" possono essere lette nella memoria cache e una o più righe cache possono essere scaricate nella memoria principale.

Combinazione del modello di memoria Java e dell'architettura hardware della memoria

Come già accennato, il modello di memoria Java e l'architettura hardware della memoria sono diversi. L'architettura hardware non distingue tra stack di thread e heap. Sull'hardware, lo stack di thread e HEAP (heap) risiedono nella memoria principale.

Parti di stack e thread heap possono talvolta essere presenti nelle cache e nei registri interni della CPU. Questo è mostrato nel diagramma:

pila di thread e HEAP

Quando oggetti e variabili possono essere memorizzati in diverse aree della memoria del computer, possono sorgere alcuni problemi. Ecco i due principali:

  • Visibilità delle modifiche apportate dal thread alle variabili condivise.
  • Race condition durante la lettura, il controllo e la scrittura di variabili condivise.

Entrambi questi problemi saranno spiegati di seguito.

Visibilità degli oggetti condivisi

Se due o più thread condividono un oggetto senza un uso appropriato della dichiarazione volatile o della sincronizzazione, le modifiche apportate all'oggetto condiviso da un thread potrebbero non essere visibili agli altri thread.

Immagina che un oggetto condiviso sia inizialmente archiviato nella memoria principale. Un thread in esecuzione su una CPU legge l'oggetto condiviso nella cache della stessa CPU. Lì apporta modifiche all'oggetto. Fino a quando la cache della CPU non è stata scaricata nella memoria principale, la versione modificata dell'oggetto condiviso non è visibile ai thread in esecuzione su altre CPU. Pertanto, ogni thread può ottenere la propria copia dell'oggetto condiviso, ogni copia sarà in una cache della CPU separata.

Il diagramma seguente illustra un profilo di questa situazione. Un thread in esecuzione sulla CPU di sinistra copia l'oggetto condiviso nella sua cache e modifica il valore di count in 2. Questa modifica è invisibile agli altri thread in esecuzione sulla CPU di destra perché l'aggiornamento a count non è stato ancora scaricato nella memoria principale.

Per risolvere questo problema, puoi usare la parola chiave volatile quando dichiari una variabile. Può garantire che una determinata variabile venga letta direttamente dalla memoria principale e venga sempre riscritta nella memoria principale quando viene aggiornata.

Condizione di gara

Se due o più thread condividono lo stesso oggetto e più di un thread aggiorna le variabili in tale oggetto condiviso, potrebbe verificarsi una race condition.

Immagina che il thread A legga la variabile count dell'oggetto condiviso nella cache del suo processore. Immagina anche che il thread B faccia la stessa cosa, ma nella cache di un altro processore. Ora il thread A aggiunge 1 al valore di count e il thread B fa lo stesso. Ora la variabile è stata aumentata due volte, separatamente di +1 nella cache di ciascun processore.

Se questi incrementi fossero eseguiti in sequenza, la variabile count verrebbe raddoppiata e riscritta nella memoria principale (valore originale + 2).

Tuttavia, due incrementi sono stati eseguiti contemporaneamente senza una corretta sincronizzazione. Indipendentemente da quale thread (A o B) scrive la sua versione aggiornata di count nella memoria principale, il nuovo valore sarà solo 1 in più rispetto al valore originale, nonostante i due incrementi.

Questo diagramma illustra l'occorrenza del problema di race condition sopra descritto:

Per risolvere questo problema, puoi utilizzare il blocco sincronizzato Java. Un blocco sincronizzato garantisce che solo un thread possa accedere a una determinata sezione critica del codice in un dato momento.

I blocchi sincronizzati garantiscono inoltre che tutte le variabili a cui si accede all'interno del blocco sincronizzato verranno lette dalla memoria principale e quando il thread esce dal blocco sincronizzato, tutte le variabili aggiornate verranno scaricate nuovamente nella memoria principale, indipendentemente dal fatto che la variabile sia dichiarata volatile o No.

Commenti
  • Popolari
  • Nuovi
  • Vecchi
Devi avere effettuato l'accesso per lasciare un commento
Questa pagina non ha ancora commenti