2012-06-26 23 views
6

dire che ho un oggetto di dati:pool di thread, dati condivisi, Java Sincronizzazione

class ValueRef { double value; }

Dove ogni oggetto dati vengono memorizzati in una collezione maestro:

Collection<ValueRef> masterList = ...;

anche io disporre di una raccolta di lavori, in cui ogni lavoro ha una raccolta locale di oggetti dati (in cui ogni oggetto dati viene visualizzato anche nello masterList):

class Job implements Runnable { 
    Collection<ValueRef> neededValues = ...; 
    void run() { 
     double sum = 0; 
     for (ValueRef x: neededValues) sum += x; 
     System.out.println(sum); 
    } 
} 

caso d'uso:

  1. for (ValueRef x: masterList) { x.value = Math.random(); }

  2. Compilare una coda di lavoro con alcuni posti di lavoro.

  3. sveglia un pool di thread

  4. Attendere ciascun lavoro è stato valutato

Nota: Durante la valutazione del lavoro, tutti i valori sono tutti costanti. Tuttavia, i thread hanno probabilmente valutato i lavori nel passato e conservano i valori memorizzati nella cache.

Domanda: qual è la quantità minima di sincronizzazione necessaria per garantire che ogni thread visualizzi gli ultimi valori?

Capisco sincronizzare dal monitor/lock-perspective, non capisco la sincronizzazione dalla cache/flush-prospettiva (cioè ciò che è garantito dal modello di memoria in entrata/uscita del blocco sincronizzato).

Per me, mi sembra di dover sincronizzare una volta nella thread che aggiorna i valori per eseguire il commit dei nuovi valori nella memoria principale e una volta per thread di lavoro, per svuotare la cache in modo da leggere i nuovi valori. Ma non sono sicuro di come sia meglio farlo.

Il mio approccio: creare un monitoraggio globale: static Object guard = new Object(); Quindi, sincronizzare il guard, durante l'aggiornamento della lista principale. Quindi, infine, prima di avviare il pool di thread, una volta per ogni thread nel pool, sincronizzare su guard in un blocco vuoto.

Ciò causa realmente un flusso completo di qualsiasi valore letto da quel thread? O solo i valori toccati all'interno del blocco di sincronizzazione? In questo caso, invece di un blocco vuoto, forse dovrei leggere ogni valore una volta in un ciclo?

Grazie per il vostro tempo.


Edit: Credo che la mia domanda si riduce a, una volta che esce da un blocco sincronizzato, non ogni prima lettura (dopo quel punto) vai a memoria principale? Indipendentemente da ciò su cui sono sincronizzato?

+0

Sembra quasi un luogo perfetto per sfruttare la parola chiave volatile – ControlAltDel

+0

Sto scrivendo una volta (in modo efficace-costante), ma potenzialmente leggendo milioni di volte. Volatile non viene mai memorizzato nella cache locale. Se avessi creato il pool di thread ogni volta, il codice funzionerebbe perfettamente senza la sincronizzazione/volatile (poiché non esisterebbe alcuna cache precedente). –

+0

Qui non vedo un bisogno di volatile. Se ValueRef è effettivamente immutabile, rendilo semplicemente immutabile. Usa doppio. Crea una nuova collezione per ogni lavoro prima che sia pianificato e includilo in unmodifiableCollection (solo come promemoria). Quale problema prevedi? –

risposta

3

Non importa che i thread di un pool di thread abbiano valutato alcuni lavori in passato.

Javadoc di Executor dice: effetti consistenza

memoria: le azioni in un filo prima di inviare un oggetto Runnable all'esecutore accadere, prima della sua esecuzione inizia, forse in un altro thread.

Quindi, se si utilizza l'implementazione del pool di thread standard e si modificano i dati prima di inoltrare i lavori, non ci si deve preoccupare degli effetti di visibilità della memoria.

+0

Questo perché ... nei thread di lavoro c'è un blocco di sincronizzazione in attesa di nuovi lavori? E quando quel blocco esce, l'intera cache del thread viene cancellata? Potrei semplicemente sincronizzare qualcosa di casuale e ottenere lo stesso effetto? –

+0

@AndrewRaffensperger: Non importa come è implementato: c'è una garanzia e dovrebbe essere fornito. Riguardo l'ultima domanda - fondamentalmente così, ma non ha senso: senza ulteriori mezzi di sincronizzazione non si può dire che i blocchi sincronizzati nei thread di lavoro eseguiti dopo il blocco sincronizzato nel thread principale; con ulteriori mezzi di sincronizzazione è ridondante. – axtavt

2

Quello che state pianificando sembra sufficiente. Dipende da come pensi di "svegliare il pool di thread".

Il modello di memoria Java prevede che tutte le scritture eseguite da un thread prima di immettere un blocco synchronized siano visibili ai thread che successivamente si sincronizzano su quel blocco.

Quindi, se si è sicuri che i thread di lavoro siano bloccati in una chiamata wait() (che deve essere all'interno di un blocco synchronized) durante l'aggiornamento dell'elenco principale, quando si attivano e diventano eseguibili, le modifiche apportate dal il thread principale sarà visibile a questi thread.

Tuttavia, è consigliabile applicare le utilità di concorrenza di livello superiore nel pacchetto java.util.concurrent. Questi saranno più robusti della tua soluzione e sono un buon posto per imparare la concorrenza prima di scavare più a fondo.


Giusto per chiarire: E 'quasi impossibile controllare thread di lavoro senza l'utilizzo di un blocco sincronizzato in cui viene eseguito un controllo per vedere se il lavoratore ha un compito da realizzare. Pertanto, tutte le modifiche apportate dal thread del controller al processo avvengono prima che il thread worker si renda attivo. È necessario un blocco synchronized o almeno una variabile volatile che funga da barriera di memoria; tuttavia, non riesco a pensare a come creare un pool di thread utilizzando uno di questi.

Come esempio dei vantaggi di utilizzare il pacchetto java.util.concurrency, considerare questo: è possibile utilizzare un blocco synchronized con una chiamata wait() in esso, o un ciclo busy-wait con una variabile volatile. A causa del sovraccarico del contesto che passa tra i thread, un'attesa impegnata può effettivamente funzionare meglio in determinate condizioni — non è necessaria l'idea orribile che si potrebbe assumere a prima vista.

Se si utilizzano le utilità di concorrenza (in questo caso, probabilmente uno ExecutorService), è possibile effettuare la selezione migliore per il caso specifico, tenendo conto dell'ambiente, della natura dell'attività e delle esigenze degli altri thread in un dato momento. Raggiungere da solo quel livello di ottimizzazione è un sacco di lavoro inutile.

+0

Non posso permettermi il sovraccarico di java.util.concurrent. I dati nel mio esempio vengono aggiornati una volta, quindi diventano "costanti" durante la valutazione multi-thread. Sono interessato a come questi dati diventano visibili agli altri thread preesistenti. Sembra che qualsiasi blocco di sincronizzazione, anche senza una relazione sincronizzata prima-accade, causi questa visibilità. O forse accade - prima non richiede alcuna sincronizzazione esplicita e "nessun lavoro viene eseguito fino a quando non vengono apportate tutte le modifiche di valore". –

+1

@AndrewRaffensperger Right. Se è tutto ciò di cui hai bisogno, esiste un'utilità 'java.util.concurrent' con il minimo di spese necessarie per la correttezza. È un errore supporre che le utilità di concorrenza abbiano un overhead più elevato; di fatto, forniscono accesso a strumenti di concorrenza ad alte prestazioni come il confronto e lo swap. L'implementazione di questo te stesso in Java sarà più lenta del codice nativo ottimizzato dietro le classi 'AtomicXXX'. Ci sono vantaggi di prestazioni simili nella maggior parte delle altre utility. – erickson

1

Perché non rendere immutabile Collection<ValueRef> e ValueRef o almeno non modificare i valori nella raccolta dopo aver pubblicato il riferimento alla raccolta. Quindi non ti preoccuperai della sincronizzazione.

Ecco quando si desidera modificare i valori della raccolta, creare una nuova raccolta e inserire nuovi valori in essa. Una volta che i valori sono stati impostati, passare il riferimento di raccolta a nuovi oggetti di lavoro.

L'unica ragione per non farlo sarebbe se la dimensione della raccolta è così grande che si adatta a malapena alla memoria e non ci si può permettere di avere due copie, o lo scambio delle raccolte causerebbe troppo lavoro per la Garbage Collector (prova che uno di questi è un problema prima di utilizzare una struttura di dati mutabile per il codice con thread).

+0

Esatto, potrei sempre ricostruire i ValueRef o ricostruire il pool di thread e il mio problema scomparirà. Ma nella mia effettiva implementazione, la struttura dei dati è molto complessa e il codice è chiamato abbastanza frequentemente che ricostruendo il pool di thread ogni valutazione sarebbe eccessiva. –

Problemi correlati