Le variabili statiche sono condivise tra i thread?


95

Il mio insegnante in un corso Java di livello superiore sul threading ha detto qualcosa di cui non ero sicuro.

Ha affermato che il codice seguente non aggiorna necessariamente la readyvariabile. Secondo lui, i due thread non condividono necessariamente la variabile statica, in particolare nel caso in cui ogni thread (thread principale rispetto a ReaderThread) è in esecuzione sul proprio processore e quindi non condivide gli stessi registri / cache / ecc. E una CPU non aggiornerà l'altro.

In sostanza, ha detto che è possibile che readyvenga aggiornato nel thread principale, ma NON in ReaderThread, in modo che ReaderThreadsi ripeta all'infinito.

Ha anche affermato che era possibile stampare 0o 42. Capisco come 42potrebbe essere stampato, ma non 0. Ha detto che questo sarebbe il caso quando la numbervariabile è impostata sul valore predefinito.

Ho pensato che forse non è garantito che la variabile statica venga aggiornata tra i thread, ma questo mi sembra molto strano per Java. La creazione di readyvolatile risolve questo problema?

Ha mostrato questo codice:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}

La visibilità delle variabili non locali non dipende dal fatto che siano variabili statiche, campi oggetto o elementi di array, hanno tutte le stesse considerazioni. (Con il problema che gli elementi dell'array non possono essere resi volatili.)
Paŭlo Ebermann

1
chiedi al tuo insegnante che tipo di architettura ritiene che sarebbe possibile vedere "0". Tuttavia, in teoria ha ragione.
bestsss

4
@bestsss Fare questo tipo di domanda avrebbe rivelato all'insegnante che aveva perso completamente il punto di ciò che stava dicendo. Il punto è che i programmatori competenti capiscono cosa è garantito e cosa non lo è e non si affidano a cose che non lo sono, almeno non senza capire esattamente cosa non è garantito e perché.
David Schwartz

Sono condivisi tra tutto ciò che viene caricato dallo stesso class loader. Compresi i thread.
Marchese di Lorne

Il tuo insegnante (e la risposta accettata) hanno ragione al 100%, ma dirò che accade raramente: questo è il tipo di problema che si nasconderà per anni e si manifesterà solo quando sarebbe più dannoso. Anche i test brevi che cercano di esporre il problema tendono ad agire come se tutto andasse bene (probabilmente perché non hanno tempo per la JVM di fare molte ottimizzazioni), quindi è davvero un buon problema di cui essere consapevoli.
Bill K

Risposte:


75

Non c'è niente di speciale nelle variabili statiche quando si tratta di visibilità. Se sono accessibili, qualsiasi thread può raggiungerli, quindi è più probabile che tu veda problemi di concorrenza perché sono più esposti.

Esiste un problema di visibilità imposto dal modello di memoria della JVM. Ecco un articolo che parla del modello di memoria e di come le scritture diventano visibili ai thread . Non puoi contare sui cambiamenti che un thread rende visibili ad altri thread in modo tempestivo (in realtà la JVM non ha alcun obbligo di rendere visibili tali modifiche a te, in qualsiasi periodo di tempo), a meno che tu non stabilisca una relazione accade prima .

Ecco una citazione da quel link (fornita nel commento di Jed Wesley-Smith):

Il capitolo 17 della specifica del linguaggio Java definisce la relazione accade prima sulle operazioni di memoria come le letture e le scritture di variabili condivise. I risultati di una scrittura da un thread sono garantiti per essere visibili a una lettura da un altro thread solo se l'operazione di scrittura avviene prima dell'operazione di lettura. I costrutti sincronizzati e volatili, così come i metodi Thread.start () e Thread.join (), possono formare relazioni accade prima. In particolare:

  • Ogni azione in un thread avviene prima di ogni azione in quel thread che viene dopo nell'ordine del programma.

  • Uno sblocco (blocco sincronizzato o uscita dal metodo) di un monitor si verifica prima di ogni blocco successivo (blocco sincronizzato o entrata del metodo) di quello stesso monitor. E poiché la relazione accade prima è transitiva, tutte le azioni di un thread prima dello sblocco avvengono prima di tutte le azioni successive a qualsiasi blocco di thread che controlla.

  • Viene eseguita una scrittura in un campo volatile, prima di ogni lettura successiva di quello stesso campo. Le scritture e le letture di campi volatili hanno effetti di consistenza della memoria simili a quelli in entrata e in uscita dai monitor, ma non comportano il blocco dell'esclusione reciproca.

  • Una chiamata per iniziare su un thread si verifica prima di qualsiasi azione nel thread avviato.

  • Tutte le azioni in un thread avvengono prima che qualsiasi altro thread ritorni correttamente da un join su quel thread.


3
In pratica, "in modo tempestivo" e "mai" sono sinonimi. Sarebbe molto probabile che il codice precedente non terminasse mai.
TREE

4
Inoltre, questo dimostra un altro anti-pattern. Non utilizzare volatile per proteggere più di un pezzo di stato condiviso. Qui, number e ready sono due elementi di stato e per aggiornarli / leggerli entrambi in modo coerente, è necessaria una sincronizzazione effettiva.
TREE

5
La parte relativa al diventare alla fine visibile è sbagliata. Senza alcuna relazione esplicita accade prima non vi è alcuna garanzia che qualsiasi scrittura sarà mai vista da un altro thread, poiché il JIT ha il diritto di inserire la lettura in un registro e quindi non vedrai mai alcun aggiornamento. Qualsiasi carico finale è fortuna e non dovrebbe essere invocato.
Jed Wesley-Smith

2
"a meno che non utilizzi la parola chiave volatile o sincronizza." dovrebbe leggere "a meno che non ci sia una relazione importante prima che chi scrive e chi legge" e questo link: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith,

2
@bestsss ben individuato. Purtroppo ThreadGroup è rotto in tanti modi.
Jed Wesley-Smith

37

Stava parlando di visibilità e di non essere preso alla lettera.

Le variabili statiche sono effettivamente condivise tra i thread, ma le modifiche apportate in un thread potrebbero non essere immediatamente visibili a un altro thread, facendo sembrare che ci siano due copie della variabile.

Questo articolo presenta una visione coerente con il modo in cui ha presentato le informazioni:

Per prima cosa, devi capire qualcosa sul modello di memoria Java. Ho faticato un po 'nel corso degli anni a spiegarlo brevemente e bene. Ad oggi, il modo migliore che mi viene in mente per descriverlo è se lo immagini in questo modo:

  • Ogni thread in Java si svolge in uno spazio di memoria separato (questo è chiaramente falso, quindi abbi pazienza su questo).

  • È necessario utilizzare meccanismi speciali per garantire che la comunicazione avvenga tra questi thread, come si farebbe su un sistema di passaggio di messaggi.

  • Le scritture di memoria che avvengono in un thread possono "trapelare" ed essere viste da un altro thread, ma questo non è affatto garantito. Senza una comunicazione esplicita, non è possibile garantire quali scritture vengono visualizzate da altri thread, o anche l'ordine in cui vengono visualizzate.

...

modello di filo

Ma ancora una volta, questo è semplicemente un modello mentale per pensare a threading e volatile, non letteralmente a come funziona la JVM.


12

In fondo è vero, ma in realtà il problema è più complesso. La visibilità dei dati condivisi può essere influenzata non solo dalle cache della CPU, ma anche dall'esecuzione fuori ordine delle istruzioni.

Pertanto Java definisce un modello di memoria , che indica in quali circostanze i thread possono vedere lo stato coerente dei dati condivisi.

Nel tuo caso particolare, l'aggiunta volatilegarantisce visibilità.


8

Sono "condivisi" ovviamente nel senso che si riferiscono entrambi alla stessa variabile, ma non vedono necessariamente gli aggiornamenti l'uno dell'altro. Questo è vero per qualsiasi variabile, non solo statica.

E in teoria, le scritture effettuate da un altro thread possono apparire in un ordine diverso, a meno che le variabili non siano dichiarate volatileo le scritture siano esplicitamente sincronizzate.


4

All'interno di un singolo programma di caricamento classi, i campi statici sono sempre condivisi. Per definire in modo esplicito l'ambito dei dati per i thread, è consigliabile utilizzare una struttura come ThreadLocal.


2

Quando si inizializza la variabile di tipo primitivo statico, java default assegna un valore per le variabili statiche

public static int i ;

quando si definisce la variabile in questo modo il valore predefinito di i = 0; ecco perché c'è la possibilità di ottenere 0. quindi il thread principale aggiorna il valore di boolean pronto a vero. poiché ready è una variabile statica, il thread principale e l'altro thread fanno riferimento allo stesso indirizzo di memoria, quindi la variabile ready cambia. così il thread secondario esce dal ciclo while e stampa il valore. quando si stampa il valore il valore inizializzato di numero è 0. se il processo del thread è passato mentre il ciclo prima del thread principale aggiorna la variabile del numero. quindi c'è la possibilità di stampare 0


-2

@dontocsata puoi tornare dal tuo insegnante e istruirlo un po ':)

poche note dal mondo reale e indipendentemente da ciò che vedi o ti viene detto. NOTA: le parole seguenti riguardano questo caso particolare nell'ordine esatto mostrato.

Le seguenti 2 variabili risiederanno sulla stessa riga della cache praticamente in qualsiasi architettura conosciuta.

private static boolean ready;  
private static int number;  

Thread.exit(thread principale) è garantito per uscire ed exitè garantito per causare un limite di memoria, a causa della rimozione del thread del gruppo di thread (e molti altri problemi). (è una chiamata sincronizzata e non vedo un unico modo per essere implementata senza la parte di sincronizzazione poiché anche il ThreadGroup deve terminare se non vengono lasciati thread daemon, ecc.).

Il thread avviato ReaderThreadmanterrà in vita il processo poiché non è un daemon! Quindi readye numberverranno scaricati insieme (o il numero prima se si verifica un cambio di contesto) e non c'è un vero motivo per riordinare in questo caso almeno non riesco nemmeno a pensarne uno. Avrai bisogno di qualcosa di veramente strano per vedere qualcosa ma 42. Di nuovo presumo che entrambe le variabili statiche saranno nella stessa riga della cache. Non riesco proprio a immaginare una linea di cache lunga 4 byte O una JVM che non li assegna in un'area continua (linea di cache).


3
@bestsss mentre questo è tutto vero oggi, si basa sull'attuale implementazione di JVM e sull'architettura hardware per la sua verità, non sulla semantica del programma. Ciò significa che il programma è ancora guasto, anche se potrebbe funzionare. È facile trovare una variante banale di questo esempio che in realtà fallisce nei modi specificati.
Jed Wesley-Smith

1
Ho detto che non segue le specifiche, tuttavia come insegnante almeno trova un esempio adatto che può effettivamente fallire su alcune architetture di base, quindi l'esempio è in qualche modo reale.
bestsss

6
Forse il peggior consiglio su come scrivere codice thread-safe che abbia mai visto.
Lawrence Dol

4
@Bestsss: La semplice risposta alla tua domanda è semplicemente questa: "Codice in base alle specifiche e al documento, non agli effetti collaterali del tuo particolare sistema o implementazione". Ciò è particolarmente importante in una piattaforma di macchina virtuale progettata per essere indipendente dall'hardware sottostante.
Lawrence Dol

1
@Bestsss: Il punto dell'insegnante è che (a) il codice potrebbe funzionare bene quando lo testate e (b) il codice è rotto perché dipende dal funzionamento dell'hardware e non dalle garanzie delle specifiche. Il punto è che sembra che sia OK, ma non lo è.
Lawrence Dol
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.