2010-03-11 12 views
9

Questo è un problema che riscontro frequentemente nel lavorare con sistemi più complessi e che non ho mai trovato un buon modo per risolvere. Di solito comporta variazioni sul tema di un oggetto condiviso la cui costruzione e inizializzazione sono necessariamente due passaggi distinti. Ciò è generalmente dovuto a requisiti di architettura, simili alle applet, quindi le risposte che suggeriscono di consolidare la costruzione e l'inizializzazione non sono utili. I sistemi devono avere come target Java 4 al massimo, quindi le risposte che suggeriscono che il supporto disponibile solo in JVM successive non sono utili.Come posso creare un valore read-many write-once thread-safe in Java?

A titolo di esempio, diciamo che ho una classe che è strutturato per adattarsi in un framework applicativo in questo modo:

public class MyClass 
{ 

private /*ideally-final*/ SomeObject someObject; 

MyClass() { 
    someObject=null; 
    } 

public void startup() { 
    someObject=new SomeObject(...arguments from environment which are not available until startup is called...); 
    } 

public void shutdown() { 
    someObject=null; // this is not necessary, I am just expressing the intended scope of someObject explicitly 
    } 
} 

non posso fare someObject finale in quanto non può essere impostata fino all'avvio() è invocato. Ma mi piacerebbe davvero che riflettesse la sua semantica di sola scrittura e potessi accedere direttamente da più thread, preferibilmente evitando la sincronizzazione.

L'idea è quello di esprimere e far rispettare un certo grado di finalness, Suppongo che avrei potuto creare un contenitore generico, in questo modo (UPDATE - corretto sematics threading di questa classe):

public class WormRef<T> 
{ 
private volatile T      reference;        // wrapped reference 

public WormRef() { 
    reference=null; 
    } 

public WormRef<T> init(T val) { 
    if(reference!=null) { throw new IllegalStateException("The WormRef container is already initialized"); } 
    reference=val; 
    return this; 
    } 

public T get() { 
    if(reference==null) { throw new IllegalStateException("The WormRef container is not initialized"); } 
    return reference; 
    } 

} 

e poi in MyClass, sopra, fare:

private final WormRef<SomeObject> someObject; 

MyClass() { 
    someObject=new WormRef<SomeObject>(); 
    } 

public void startup() { 
    someObject.init(new SomeObject(...)); 
    } 

public void sometimeLater() { 
    someObject.get().doSomething(); 
    } 

che solleva alcune domande per me:

  1. C'è un modo migliore o un oggetto Java esistente (dovrebbe essere disponibile in Java 4)?

In secondo luogo, in termini di sicurezza filo:

  1. È questo thread-safe condizione che nessun altro thread accede someObject.get() fino a dopo la sua set() è stato chiamato. Gli altri thread invocheranno solo metodi su MyClass tra startup() e shutdown() - il framework lo garantisce.
  2. Dato il contenitore WormReference completamente non sincronizzato, è sempre possibile in JMM visualizzare un valore di object che non è nullo nè un riferimento a SomeObject? In altre parole, il JMM garantisce sempre che nessun thread possa osservare la memoria di un oggetto come qualsiasi valore presente nell'heap quando l'oggetto è stato allocato. Credo che la risposta sia "Sì" perché l'allocazione azzera in modo esplicito la memoria allocata, ma il caching della CPU può far sì che qualcos'altro venga osservato in una determinata posizione di memoria?
  3. È sufficiente rendere volatile WormRef.reference per garantire una semantica multithread corretta?

Nota la spinta principale di questo problema è come esprimere e far rispettare la finalness di someObject senza essere in grado di effettivamente segnare final; secondario è ciò che è necessario per la sicurezza del filo. Cioè, non rimanere troppo bloccato sull'aspetto di sicurezza del thread di questo.

+0

Suppongo che dire "non avvia gli altri thread finché non è inizializzata" non sta per essere accettabile? – MSN

+0

@MSN: In realtà, nella maggior parte dei casi è un dato di fatto che gli altri thread non saranno nemmeno avviati fino a dopo l'inizializzazione. –

+0

A tutti, ho cancellato la mia risposta perché l'OP stava insultando che ho modificato la sua domanda per imporre la mia risposta. Questo non ha senso. – BalusC

risposta

0

Questa è la mia risposta definitiva, Regis :

/** 
* Provides a simple write-one, read-many wrapper for an object reference for those situations 
* where you have an instance variable which you would like to declare as final but can't because 
* the instance initialization extends beyond construction. 
* <p> 
* An example would be <code>java.awt.Applet</code> with its constructor, <code>init()</code> and 
* <code>start()</code> methods. 
* <p> 
* Threading Design : [ ] Single Threaded [x] Threadsafe [ ] Immutable [ ] Isolated 
* 
* @since   Build 2010.0311.1923 
*/ 

public class WormRef<T> 
extends Object 
{ 

private volatile T      reference;        // wrapped reference 

public WormRef() { 
    super(); 

    reference=null; 
    } 

public WormRef<T> init(T val) { 
    // Use synchronization to prevent a race-condition whereby the following interation could happen between three threads 
    // 
    // Thread 1  Thread 2  Thread 3 
    // --------------- --------------- --------------- 
    // init-read null 
    //     init-read null 
    // init-write A 
    //         get A 
    //     init-write B 
    //         get B 
    // 
    // whereby Thread 3 sees A on the first get and B on subsequent gets. 
    synchronized(this) { 
     if(reference!=null) { throw new IllegalStateException("The WormRef container is already initialized"); } 
     reference=val; 
     } 
    return this; 
    } 

public T get() { 
    if(reference==null) { throw new IllegalStateException("The WormRef container is not initialized"); } 
    return reference; 
    } 

} // END PUBLIC CLASS 

(1) conferire il game show "Così si vuole essere un milionario", ospitato da Regis Philburn.

1

In teoria sarebbe sufficiente riscrivere startup() come segue:

public synchronized void startup() { 
    if (someObject == null) someObject = new SomeObject(); 
} 

proposito, anche se il WoRmObject è finale, fili possono comunque invocare set() più volte. Avrai davvero bisogno di aggiungere un po 'di sincronizzazione.

aggiornamento: ho giocato un po 'intorno ad esso e creato un SSCCE, può risultare utile per giocare un po' con essa :)

package com.stackoverflow.q2428725; 

import java.util.concurrent.Callable; 
import java.util.concurrent.CountDownLatch; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Future; 
import java.util.concurrent.ScheduledExecutorService; 
import java.util.concurrent.TimeUnit; 

public class Test { 

    public static void main(String... args) throws Exception { 
     Bean bean = new Bean(); 
     ScheduledExecutorService executor = Executors.newScheduledThreadPool(4); 
     executor.schedule(new StartupTask(bean), 2, TimeUnit.SECONDS); 
     executor.schedule(new StartupTask(bean), 2, TimeUnit.SECONDS); 
     Future<String> result1 = executor.submit(new GetTask(bean)); 
     Future<String> result2 = executor.submit(new GetTask(bean)); 
     System.out.println("Result1: " + result1.get()); 
     System.out.println("Result2: " + result2.get()); 
     executor.shutdown(); 
    } 

} 

class Bean { 

    private String property; 
    private CountDownLatch latch = new CountDownLatch(1); 

    public synchronized void startup() { 
     if (property == null) { 
      System.out.println("Setting property."); 
      property = "foo"; 
      latch.countDown(); 
     } else { 
      System.out.println("Property already set!"); 
     } 
    } 

    public String get() { 
     try { 
      latch.await(); 
     } catch (InterruptedException e) { 
      // handle. 
     } 
     return property; 
    } 

} 

class StartupTask implements Runnable { 

    private Bean bean; 

    public StartupTask(Bean bean) { 
     this.bean = bean; 
    } 

    public void run() { 
     System.out.println("Starting up bean..."); 
     bean.startup(); 
     System.out.println("Bean started!"); 
    } 

} 

class GetTask implements Callable<String> { 

    private Bean bean; 

    public GetTask(Bean bean) { 
     this.bean = bean; 
    } 

    public String call() { 
     System.out.println("Getting bean property..."); 
     String property = bean.get(); 
     System.out.println("Bean property got!"); 
     return property; 
    } 

} 

Il CountDownLatch causerà tutti await() chiamate per bloccare fino il conto alla rovescia raggiunge lo zero.

+1

No meno che get() sia anche sincronizzato. –

+0

Potrei sbagliarmi, ma penso che quando è garantito, che l'avvio sia terminato prima che qualsiasi thread acceda a get(), la sincronizzazione di get potrebbe essere sufficiente. –

+0

@ Alex: punto giusto. Dovresti quindi bloccare il 'get()' finché 'someObject' non è più nullo. L'unico modo non sincronizzato (e antipatico?) Per questo sarebbe aggiungere 'while (someObject == null);' all'inizio del metodo 'get()'. – BalusC

0
  1. come su sincronizzazione?
  2. No, non è sicuro per thread. Senza la sincronizzazione, il nuovo stato della variabile potrebbe non essere mai comunicato ad altri thread.
  3. Sì, per quanto ne so i riferimenti sono atomici, quindi vedrai null o il riferimento. Si noti che lo stato dell'oggetto si fa riferimento è una storia completamente diversa
1

Vorrei iniziare dichiarando il tuo someObjectvolatile.

private volatile SomeObject someObject; 

parola chiave volatile crea una barriera di memoria, il che significa thread separati sarà sempre vedere Memoria aggiornato quando si fa riferimento someObject.

Nell'implementazione corrente alcuni thread potrebbero ancora vedere someObject come null anche dopo che è stato chiamato startup.

In realtà questa tecnica volatile viene utilizzata molto dalle raccolte dichiarate nel pacchetto java.util.concurrent.

E come alcuni altri poster suggeriscono qui, se tutti gli altri fallimenti tornano alla piena sincronizzazione.

+1

Volitale non impedisce di chiamare 'get()' prima di 'startup()'. Impedisce solo che il riferimento sia acceduto simultaneamente (cioè il suo accesso è sincronizzato) e ciò può causare un calo delle prestazioni in 'get()' come se fosse 'sincronizzato '. – BalusC

+0

@BalusC: buon punto sul chiamare 'get()' prima di 'init()'; Dovrò fare anche un test per null nel getter. –

1

vorrei togliere il metodo setter in WoRmObject, e fornire un init sincronizzato() metodo che genera un'eccezione if (object != null)

+0

Grazie - In realtà intendevo controllare che non fosse già impostato. E mi piace molto il suggerimento 'init()' (in realtà è esattamente quello che faccio normalmente per l'inizializzazione una tantum). Codice aggiornato –

0

Hai bisogno di un ThreadLocal che consente solo il valore di ogni thread per impostare una volta?

+0

Questo non funzionerebbe; l'oggetto deve essere condiviso tra più thread. –

1

Considerare l'utilizzo di AtomicReference come delegato in questo oggetto-contenitore che si sta tentando di creare. Ad esempio:

public class Foo<Bar> { 
private final AtomicReference<Bar> myBar = new AtomicReference<Bar>(); 
public Bar get() { 
    if (myBar.get()==null) myBar.compareAndSet(null,init()); 
    return myBar.get(); 
} 

Bar init() { /* ... */ } 
//... 
} 

MODIFICA: Questo verrà impostato una volta, con un metodo di inizializzazione pigro. Non è perfetto per bloccare più chiamate a un (presumibilmente costoso) init(), ma potrebbe essere peggio. È possibile inserire l'istanza di myBar nel costruttore e quindi aggiungere un costruttore che consenta anche l'assegnazione, se fornite le informazioni corrette.

C'è una discussione generale sull'istanza dei thread singleton (che è abbastanza simile al tuo problema), ad esempio, this site.

+0

Un buon suggerimento - +1. Ma AtomicReference sotto la cappa mantiene un riferimento volatile, quindi in questo caso sembra che sarebbe solo un doppio avvolgimento senza alcun guadagno rendendo il riferimento volatile. Inoltre, 'init()' non è (sempre) costoso; è solo che deve essere differito fino a dopo la costruzione. –

+0

sicuro, ma gestisce una parte della logica del boilerplate associata a vari test per quando init(), ecc. – Carl

+0

Vedo dove stai andando con quello; ma l'oggetto WORM non può inizializzare da solo, ha bisogno di qualcosa di esterno per chiamare init() con l'oggetto che deve avvolgere. –

0

Ci sono molti modi sbagliati per eseguire l'istanza lazy, specialmente in Java.

In breve, l'approccio ingenuo è creare un oggetto privato, un metodo di inizializzazione pubblico sincronizzato e un metodo di acquisizione pubblico non sincronizzato che esegue un controllo Null sul proprio oggetto e chiama init, se necessario. Le complessità del problema arrivano nell'eseguire il controllo Null in modo thread-safe.

Questo articolo dovrebbe essere di uso: http://en.wikipedia.org/wiki/Double-checked_locking

questo argomento specifico, in Java, è discusso in modo approfondito nel 'programmazione concorrente in Java' di Doug Lea, che è un po 'fuori moda, e in 'Java Concurrency in Practice 'co-autore di Lea e altri. In particolare, CPJ è stato pubblicato prima del rilascio di Java 5, che ha migliorato significativamente i controlli di concorrenza di Java.

Posso pubblicare più specifiche quando torno a casa e ho accesso a detti libri.

1

È molto probabilmente thread-safe, dalla descrizione del framework. Deve esserci stata una barriera di memoria da qualche parte tra chiamare myobj.startup() e rendere disponibile myobj ad altri thread. Ciò garantisce che le scritture in startup() siano visibili ad altri thread. Quindi non devi preoccuparti della sicurezza dei thread perché il framework lo fa. Non c'è il pranzo libero però; ogni volta che un altro thread ottiene l'accesso a myobj attraverso il framework, deve coinvolgere la sincronizzazione o la lettura volatile.

Se si esamina il framework e si elenca il codice nel percorso, si vedrà sync/volatile in punti appropriati che rendono sicuro il thread del codice. Cioè, se il framework è correttamente implementato.

Esaminiamo un tipico esempio di swing, in cui un thread di lavoro esegue alcuni calcoli, salva i risultati in una variabile globale x, quindi invia un evento di ridisegno. Il thread della GUI al ricevimento dell'evento repaint, legge i risultati dalla variabile globale x e ripristina di conseguenza.

Né il thread di lavoro né il codice ridisegno eseguono alcuna sincronizzazione o lettura/scrittura volatile su alcunché. Ci devono essere decine di migliaia di implementazioni come questa. Fortunatamente sono tutti sicuri, anche se i programmatori non hanno prestato particolare attenzione. Perché? Perché la coda degli eventi è sincronizzata; abbiamo una bella happends-prima catena:

write x - insert event - read event - read x 

Quindi scrivere e leggere xx siano correttamente sincronizzati, in modo implicito tramite framework evento.

+0

Buon punto. E invero vero nel mio particolare scenario d'uso. Più in generale, sono ancora incline a rendere volatile il riferimento nel wrapper WORM, quindi è sempre sicuro, anche se in questi casi in cui ho inizializzato la divisione, sembrano essere sempre nel contesto di un framework correttamente sincronizzato. –

0

Solo la mia piccola versione basata su AtomicReference. Probabilmente non è il migliore, ma credo che ad essere pulito e facile da usare:

public static class ImmutableReference<V> { 
    private AtomicReference<V> ref = new AtomicReference<V>(null); 

    public boolean trySet(V v) 
    { 
     if(v == null) 
      throw new IllegalArgumentException("ImmutableReference cannot hold null values"); 

     return ref.compareAndSet(null, v); 
    } 

    public void set(V v) 
    { 
     if(!trySet(v)) throw new IllegalStateException("Trying to modify an immutable reference"); 
    } 

    public V get() 
    { 
     V v = ref.get(); 
     if(v == null) 
      throw new IllegalStateException("Not initialized immutable reference."); 

     return v; 
    } 

    public V tryGet() 
    { 
     return ref.get(); 
    } 
} 
+0

Non va bene - deve essere eseguito in Java 4 (vedere la domanda). –

0

Prima domanda: Perché non puoi semplicemente fare avviare un metodo privato, chiamato nel costruttore, allora può essere finale. Ciò garantirebbe la sicurezza del thread dopo la chiamata del costruttore, poiché è invisibile prima e viene letto solo dopo il ritorno del costruttore.Oppure riformattare la struttura della classe in modo che il metodo di avvio possa creare l'oggetto MyClass come parte del suo costruttore. In alcuni casi questo caso particolare sembra un caso di struttura povera, in cui si vuole davvero renderlo definitivo e immutabile.

L'approccio facile, se la classe è immutabile e viene letto solo dopo che è stato creato, quindi inserirlo in un elenco immutabile da guava. È anche possibile creare il proprio wrapper immutabile che copia in modo difensivo quando viene richiesto di restituire il riferimento, in modo da impedire a un client di modificare il riferimento. Se è immutabile internamente, non è necessaria alcuna ulteriore sincronizzazione e sono consentite letture non sincronizzate. È possibile impostare il wrapper per la copia in modo difensivo su richiesta, quindi anche i tentativi di scrittura su di esso falliscono in modo pulito (semplicemente non fanno nulla). Potrebbe essere necessaria una barriera di memoria, oppure potresti essere in grado di eseguire l'inizializzazione pigra, sebbene si noti che l'inizializzazione pigra potrebbe richiedere un'ulteriore sincronizzazione, in quanto è possibile che durante l'elaborazione dell'oggetto vengano generate diverse richieste di lettura non sincronizzate.

L'approccio leggermente più coinvolto comporterebbe l'utilizzo di un'enumerazione. Poiché le enumerazioni sono garantite singleton, non appena l'enumerazione viene creata, viene risolta per sempre. Devi ancora assicurarti che l'oggetto sia internamente immutabile, ma garantisce il suo status di singleton. Senza molti sforzi

+0

Prima domanda: bene, "Questo è generalmente dovuto ai requisiti architettonici, simili alle applet, quindi le risposte che suggeriscono di consolidare la costruzione e l'inizializzazione non sono utili." –

-1

La seguente classe potrebbe rispondere alla tua domanda. Alcuni thread-sicurezza raggiunto utilizzando una variabile intermedia volatile in combinazione con valore final custode nel generico fornito. Si può prendere in considerazione un ulteriore aumento di esso utilizzando synchronized setter/getter. Spero che sia d'aiuto.

https://stackoverflow.com/a/38290652/6519864