2015-06-25 20 views
9

Che cosa significa che un riferimento deve essere assegnato atomicamente in Java?Quando un riferimento deve essere atomico?

  • capisco cosa significa per un lungo e doppio, cioè: un thread può vedere il numero parzialmente costruito,
  • ma per un oggetto non capisco poiché assegnazione non significa appena copia indicando un indirizzo in memoria

Quindi cosa poteva essere sbagliato se l'assegnazione di riferimento non era atomica in Java?

risposta

13

Ciò significa che non si otterrà mai il riferimento corrotto. Supponiamo di avere la seguente classe:

class MyClass { 
    Object obj = null; 
} 

In memoria obj è un puntatore nullo, di solito si tratta di un numero intero come 0x00000000. Poi supporre che in un thread di avere un compito:

this.obj = new Object(); 

Supponiamo che new Object() è allocata nella memoria e ha il puntatore come 0x12345678. L'atomicità di riferimento assicura che quando si controlla il obj da un altro thread si disponga o di un puntatore nullo (0x00000000) o puntatore al nuovo oggetto (0x12345678). Ma in nessun caso è possibile ottenere il riferimento parzialmente assegnato (come 0x12340000) che punta verso il nulla.

Questo potrebbe sembrare ovvio, ma tale problema può apparire in linguaggi di basso livello come C a seconda dell'architettura della CPU e dell'allineamento della memoria. Ad esempio, se il puntatore non è allineato e attraversa la linea della cache, è possibile che non venga aggiornato in modo sincrono. Per evitare tale situazione, la macchina virtuale Java allinea sempre i puntatori, in modo che non attraversino mai la linea della cache.

Così erano i riferimenti Java non atomici, ci sarebbe stata una possibilità nel dereferenziare il riferimento scritto da un altro thread che non si ottiene l'oggetto a cui è stato fatto riferimento prima o dopo l'assegnazione, ma la posizione di memoria casuale (che può portare a errore di segmentazione, heap corrotto o qualsiasi altro disastro).

+0

* riferimento parzialmente costruito *. Trovo che questa linea sia un po 'errata. – CKing

+0

@ChetanKinger: è meglio ora? –

+1

Quello che stavo cercando di indicare è che il motivo per cui abbiamo bisogno di un riferimento atomico è evitare che i thread utilizzino un riferimento a un oggetto parzialmente costruito.Invece di * riferimento parzialmente costruito *, la tua risposta dovrebbe leggere * il riferimento a un oggetto parzialmente costruito *. (Secondo me) – CKing

5

Suppongo che stiate chiedendo di AtomicReference<V>.

L'idea è che se due o più thread leggono o aggiornano il valore di una variabile di tipo di riferimento, si potrebbero ottenere risultati imprevisti. Ad esempio, supponiamo che ogni thread verifichi se alcune variabili di riferimento sono nulle e se è nullo crea un'istanza di quel tipo e aggiorna quella variabile di riferimento.

Ciò potrebbe causare la creazione di due istanze se entrambi i thread vedono che la variabile è nullo allo stesso tempo. Se il tuo codice si basa su tutti i thread che funzionano con la stessa istanza indirizzata da quella variabile, ti troverai nei guai.

Ora, se si utilizza AtomicReference<V>, è possibile risolvere questo problema utilizzando il metodo compareAndSet(V expect, V update). Quindi un thread aggiornerà la variabile solo se qualche altro thread non lo ha battuto su di esso.

Ad esempio:

static AtomicReference<MyClass> ref = new AtomicReference<>(); 

... 
// code of some thread 
MyClass obj = ref.get(); 
if (obj == null) { 
    obj = new MyClass(); 
    if (!ref.compareAndSet (null, obj)) // try to set the atomic reference to a new value 
             // only if it's still null 
     obj = ref.get(); // if some other thread managed to set it before the current thread, 
         // get the instance created by that other thread 
} 
+1

Questo non è perfetto, poiché potresti creare due oggetti 'MyClass', che potrebbero essere indesiderabili. Un semplice sincronizzato all'interno aiuterebbe. – pwes

+0

In effetti, il classico esempio di singleton non è un buon modo per dimostrarlo. – Kayaman

+0

@pwes Sì, un'istanza MyClass ridondante può essere creata da questo codice, ma non verrà assegnata al riferimento atomico e tutti i thread utilizzeranno la stessa istanza. Sono d'accordo non è un esempio perfetto. – Eran

9

Consideriamo la doppia classic checked esempio blocco di capire perché un riferimento deve essere atomica:

class Foo { 
    private Helper result; 
    public static Helper getHelper() { 
     if (result == null) {//1 
      synchronized(Foo.class) {//2 
       if (result == null) {//3 
        result = new Helper();//4 
       } 
      } 
     } 
     return result//5; 
    } 

    // other functions and members... 
} 

Consideriamo 2 discussioni che stanno andando a chiamare il getHelper metodo:

  1. Thread-1 esegue il numero di riga 1 e trova result per essere null.
  2. Thread-1 acquisisce un blocco livello di classe nella riga numero 2
  3. Thread-1 trova result essere null nella riga numero 3
  4. Thread-1 avvia un'istanza di un nuovo Helper
  5. Mentre Thread-1 è ancora istanziando un nuovo Helper sulla riga numero 4, Thread-2 esegue il numero di riga 1.

I passaggi 4 e 5 sono i punti in cui può verificarsi un'incoerenza. È possibile che al punto 4 l'oggetto non sia completamente istanziato ma che la variabile result abbia già già inserito l'indirizzo dell'oggetto Helper parzialmente creato. Se lo Step-5 esegue anche un nanosecondo prima che l'oggetto Helper sia completamente inizializzato, Thread-2 vedrà che il riferimento result non è null e potrebbe restituire un riferimento a un oggetto parzialmente creato.

Un modo per risolvere il problema è contrassegnare result come volatile o utilizzare AtomicReference.Detto questo, lo scenario sopra descritto è altamente improbabile che si verifichi nel mondo reale e ci sono modi migliori per implementare uno Singleton rispetto all'utilizzo del blocco a doppio controllo.

Here's un esempio di attuazione di interblocco ricontrollato con AtomicReference:

private static AtomicReference instance = new AtomicReference(); 

public static AtomicReferenceSingleton getDefault() { 
    AtomicReferenceSingleton ars = instance.get(); 
    if (ars == null) { 
     instance.compareAndSet(null,new AtomicReferenceSingleton()); 
     ars = instance.get(); 
    } 
    return ars; 
} 

Se siete interessati a sapere perché Fase 5 può provocare incongruenze di memoria, dare un'occhiata a this risposta (come suggerito da pwes in i commenti)

+0

@ChetanKinger - ** Un modo per risolvere il problema è contrassegnare il risultato come volatile o utilizzare un AtomicReference ** ma volatile ha solo rilevanza per le modifiche della variabile stessa, non l'oggetto a cui fa riferimento a destra? – Nirmal

+1

@ user3320018 Il codice AtomicReference utilizza 'volatile' internamente per l'oggetto di riferimento. Non ha nulla a che fare con il contenuto dell'oggetto, che deve fare lo stesso, se necessario, o usare la sincronizzazione corretta – pwes

+0

Ok cool. Un'altra domanda - quindi l'assegnazione di riferimento (non oggetto concreto su heap) richiede due operazioni, vero? – Nirmal

Problemi correlati