Il thread! = Check è sicuro?


140

So che operazioni composte come i++non sono thread-safe in quanto coinvolgono più operazioni.

Ma controllare il riferimento con se stesso è un'operazione sicura del thread?

a != a //is this thread-safe

Ho provato a programmare questo e usare più thread ma non è fallito. Immagino di non poter simulare la corsa sulla mia macchina.

MODIFICARE:

public class TestThreadSafety {
    private Object a = new Object();

    public static void main(String[] args) {

        final TestThreadSafety instance = new TestThreadSafety();

        Thread testingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                long countOfIterations = 0L;
                while(true){
                    boolean flag = instance.a != instance.a;
                    if(flag)
                        System.out.println(countOfIterations + ":" + flag);

                    countOfIterations++;
                }
            }
        });

        Thread updatingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while(true){
                    instance.a = new Object();
                }
            }
        });

        testingReferenceThread.start();
        updatingReferenceThread.start();
    }

}

Questo è il programma che sto usando per testare la sicurezza dei thread.

Comportamento strano

Quando il mio programma inizia tra alcune iterazioni, ottengo il valore del flag di output, il che significa che il !=controllo del riferimento fallisce sullo stesso riferimento. MA dopo alcune iterazioni l'output diventa un valore costante falsee quindi l'esecuzione del programma per molto tempo non genera un singolo trueoutput.

Come l'output suggerisce dopo alcune iterazioni n (non fisse) l'output sembra essere un valore costante e non cambia.

Produzione:

Per alcune iterazioni:

1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true

2
Cosa intendi con "thread-safe" in questo contesto? Stai chiedendo se è garantito che restituisca sempre false?
JB Nizet,

@JBNizet sì. Questo è quello a cui stavo pensando.
Narendra Pathai,

5
Non restituisce nemmeno false in un contesto a thread singolo. Potrebbe essere un NaN ..
Harold,

4
Probabile spiegazione: il codice è stato compilato just-in-time e il codice compilato carica la variabile una sola volta. Questo è previsto.
Marko Topolnik,

3
La stampa dei risultati individuali è un modo scadente per testare le gare. La stampa (sia la formattazione che la scrittura dei risultati) è relativamente costosa rispetto al test (e talvolta il programma finirà per bloccarsi in scrittura quando la larghezza di banda della connessione al terminale o il terminale stesso è lento). Inoltre, IO spesso contiene mutex della propria che permute l'ordine di esecuzione delle vostre discussioni (notare le linee individuali di 1234:truenon distruggere l'altro ). Un test di gara richiede un circuito interno più stretto. Stampa un riepilogo alla fine (come ha fatto qualcuno di seguito con un framework di unit test).
Ben Jackson,

Risposte:


124

In assenza di sincronizzazione questo codice

Object a;

public boolean test() {
    return a != a;
}

può produrre true. Questo è il bytecode pertest()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

come possiamo vedere, carica adue volte il campo su vari vars locali, è un'operazione non atomica, se aun cambio di thread tra di loro può produrre false.

Inoltre, qui il problema di visibilità della memoria è rilevante, non esiste alcuna garanzia che le modifiche aapportate a un altro thread siano visibili al thread corrente.


22
Sebbene sia una prova concreta, il bytecode non è in realtà una prova. Deve essere anche da qualche parte nel JLS ...
Marko Topolnik il

10
@Marko Sono d'accordo con il tuo pensiero, ma non necessariamente la tua conclusione. Per me il bytecode sopra è il modo ovvio / canonico di implementazione !=, che prevede il caricamento di LHS e RHS separatamente. E quindi se JLS non menziona nulla di specifico sull'ottimizzazione quando LHS e RHS sono sintatticamente identici, si applica la regola generale, il che significa caricare adue volte.
Andrzej Doyle,

20
In realtà, supponendo che il bytecode generato sia conforme a JLS, è una prova!
proskor,

6
@Adrian: in primo luogo: anche se tale presupposto non è valido, l'esistenza di un singolo compilatore in cui può essere valutato come "vero" è sufficiente per dimostrare che a volte può essere valutato in "vero" (anche se le specifiche lo vietano - cosa che non lo fa). In secondo luogo: Java è ben specificato e la maggior parte dei compilatori si conforma strettamente ad esso. Ha senso usarli come riferimento in questo senso. Terzo: usi il termine "JRE", ma non penso che significhi ciò che pensi significhi. . .
Ruakh,

2
@AlexanderTorstling - "Non sono sicuro che sia sufficiente per precludere un'ottimizzazione a lettura singola." Non è sufficiente In effetti, in assenza di sincronizzazione (e le relazioni extra "succede prima" che impongono), l'ottimizzazione è valida,
Stephen C

47

Il controllo è a != asicuro?

Se apuò essere potenzialmente aggiornato da un altro thread (senza una corretta sincronizzazione!), Allora No.

Ho provato a programmare questo e usare più thread ma non ci sono riuscito. Suppongo che non sia stato possibile simulare la corsa sulla mia macchina.

Questo non significa niente! Il problema è che se un'esecuzione in cui aviene aggiornato da un altro thread è consentita da JLS, il codice non è thread-safe. Il fatto che non si possa far accadere la race condition con un particolare test-case su una macchina particolare e una particolare implementazione Java, non impedisce che ciò accada in altre circostanze.

Questo significa che a! = A potrebbe tornare true.

Sì, in teoria, in determinate circostanze.

In alternativa, a != apotrebbe tornare falseanche se astava cambiando contemporaneamente.


Per quanto riguarda il "comportamento strano":

Quando il mio programma inizia tra alcune iterazioni, ottengo il valore del flag di output, il che significa che il riferimento! = Il controllo fallisce sullo stesso riferimento. MA dopo alcune iterazioni l'output diventa falso valore costante e quindi l'esecuzione del programma per molto tempo non genera un singolo output vero.

Questo comportamento "strano" è coerente con il seguente scenario di esecuzione:

  1. Il programma viene caricato e JVM inizia a interpretare i bytecode. Dato che (come abbiamo visto dall'output javap) il bytecode fa due carichi, tu (apparentemente) vedi occasionalmente i risultati della condizione di gara.

  2. Dopo un po ', il codice viene compilato dal compilatore JIT. L'ottimizzatore JIT nota che ci sono due carichi dello stesso slot di memoria ( a) vicini tra loro e ottimizza il secondo. (In effetti, c'è una possibilità che ottimizzi completamente il test ...)

  3. Ora le condizioni di gara non si manifestano più, perché non ci sono più due carichi.

Si noti che questo è tutto coerente con ciò che la JLS permette un'implementazione di Java da fare.


@kriss ha commentato così:

Questo potrebbe essere ciò che i programmatori C o C ++ chiamano "Undefined Behaviour" (dipendente dall'implementazione). Sembra che ci potrebbe essere un po 'di UB in Java in casi d'angolo come questo.

Il modello di memoria Java (specificato in JLS 17.4 ) specifica un insieme di condizioni preliminari in base al quale è garantito a un thread di vedere i valori di memoria scritti da un altro thread. Se un thread tenta di leggere una variabile scritta da un altro e queste condizioni non sono soddisfatte, allora ci possono essere un numero di esecuzioni possibili ... alcune delle quali sono probabilmente errate (dal punto di vista dei requisiti dell'applicazione). In altre parole, viene definito l' insieme di comportamenti possibili (ovvero l'insieme di "esecuzioni ben formate"), ma non possiamo dire quale di questi comportamenti si verificherà.

Al compilatore è consentito combinare e riordinare i carichi e salvare (e fare altre cose) purché l'effetto finale del codice sia lo stesso:

  • quando eseguito da un singolo thread, e
  • quando eseguito da thread diversi che si sincronizzano correttamente (secondo il modello di memoria).

Ma se il codice non si sincronizza correttamente (e quindi le relazioni "succede prima" non limitano sufficientemente l'insieme di esecuzioni ben formate) al compilatore è consentito riordinare carichi e archivi in ​​modo da dare risultati "errati". (Ma questo in realtà sta solo dicendo che il programma non è corretto.)


Questo significa che a != apotrebbe tornare vero?
proskor,

Volevo dire che forse sulla mia macchina non avrei potuto simulare che il codice sopra non è sicuro per i thread. Quindi forse c'è un ragionamento teorico dietro di esso.
Narendra Pathai,

@NarendraPathai - Non c'è motivo teorico per cui non puoi dimostrarlo. C'è forse un motivo pratico ... o forse non sei stato fortunato.
Stephen C

Controlla la mia risposta aggiornata con il programma che sto utilizzando. Il controllo ritorna vero a volte ma sembra che ci sia un comportamento strano nell'output.
Narendra Pathai,

1
@NarendraPathai - Vedi la mia spiegazione.
Stephen C

27

Provato con test-ng:

public class MyTest {

  private static Integer count=1;

  @Test(threadPoolSize = 1000, invocationCount=10000)
  public void test(){
    count = new Integer(new Random().nextInt());
    Assert.assertFalse(count != count);
  }

}

Ho 2 fallimenti su 10.000 invocazioni. Quindi NO , NON è thread-safe


6
Non stai nemmeno verificando l'uguaglianza ... la Random.nextInt()parte è superflua. Avresti potuto provare anche con new Object().
Marko Topolnik,

@MarkoTopolnik Controlla la mia risposta aggiornata con il programma che sto utilizzando. Il controllo ritorna vero a volte ma sembra che ci sia un comportamento strano nell'output.
Narendra Pathai,

1
Una nota a margine, gli oggetti casuali sono generalmente pensati per essere riutilizzati, non creati ogni volta che è necessario un nuovo int.
Simon Forsberg,

15

No non lo è. Per un confronto, la VM Java deve mettere i due valori per confrontare nello stack ed eseguire l'istruzione di confronto (che dipende dal tipo di "a").

La VM Java può:

  1. Leggi "a" due volte, mettine ognuna in pila e poi confronta i risultati
  2. Leggere "a" solo una volta, metterlo in pila, duplicarlo (istruzione "dup") ed eseguire il confronto
  3. Elimina completamente l'espressione e sostituiscila con false

Nel primo caso, un altro thread potrebbe modificare il valore di "a" tra le due letture.

La strategia scelta dipende dal compilatore Java e Java Runtime (in particolare il compilatore JIT). Potrebbe anche cambiare durante il runtime del programma.

Se vuoi essere sicuro di come accedere alla variabile, devi crearla volatile(una cosiddetta "barriera di mezza memoria") o aggiungere una barriera di memoria piena ( synchronized). Puoi anche usare alcune API di livello più elevato (ad esempio, AtomicIntegercome menzionato da Juned Ahasan).

Per dettagli sulla sicurezza dei thread, leggere JSR 133 ( Java Memory Model ).


Dichiarare acome volatileimplicherebbe ancora due letture distinte, con la possibilità di un cambiamento nel mezzo.
Holger,

6

È stato tutto ben spiegato da Stephen C. Per divertimento, potresti provare a eseguire lo stesso codice con i seguenti parametri JVM:

-XX:InlineSmallCode=0

Questo dovrebbe impedire l'ottimizzazione fatta da JIT (lo fa sul server hotspot 7) e vedrai trueper sempre (mi sono fermato a 2.000.000 ma suppongo che continui dopo).

Per informazioni, di seguito è riportato il codice JIT. Ad essere sincero, non leggo l'assemblaggio abbastanza fluentemente da sapere se il test è stato effettivamente eseguito o da dove provengono i due carichi. (la riga 26 è il test flag = a != ae la riga 31 è il controvento di chiusura del while(true)).

  # {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
  0x00000000027dcc80: int3   
  0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
  0x00000000027dcc8c: data32 data32 xchg ax,ax
  0x00000000027dcc90: mov    DWORD PTR [rsp-0x6000],eax
  0x00000000027dcc97: push   rbp
  0x00000000027dcc98: sub    rsp,0x40
  0x00000000027dcc9c: mov    rbx,QWORD PTR [rdx+0x8]
  0x00000000027dcca0: mov    rbp,QWORD PTR [rdx+0x18]
  0x00000000027dcca4: mov    rcx,rdx
  0x00000000027dcca7: movabs r10,0x6e1a7680
  0x00000000027dccb1: call   r10
  0x00000000027dccb4: test   rbp,rbp
  0x00000000027dccb7: je     0x00000000027dccdd
  0x00000000027dccb9: mov    r10d,DWORD PTR [rbp+0x8]
  0x00000000027dccbd: cmp    r10d,0xefc158f4    ;   {oop('javaapplication27/TestThreadSafety$1')}
  0x00000000027dccc4: jne    0x00000000027dccf1
  0x00000000027dccc6: test   rbp,rbp
  0x00000000027dccc9: je     0x00000000027dcce1
  0x00000000027dcccb: cmp    r12d,DWORD PTR [rbp+0xc]
  0x00000000027dcccf: je     0x00000000027dcce1  ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
                                                ;   {poll}
  0x00000000027dccdb: jmp    0x00000000027dccd1
  0x00000000027dccdd: xor    ebp,ebp
  0x00000000027dccdf: jmp    0x00000000027dccc6
  0x00000000027dcce1: mov    edx,0xffffff86
  0x00000000027dcce6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dcceb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=112}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dccf0: int3   
  0x00000000027dccf1: mov    edx,0xffffffad
  0x00000000027dccf6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dccfb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=128}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
  0x00000000027dcd01: int3   

1
Questo è un buon esempio del tipo di codice che JVM produrrà effettivamente quando si ha un ciclo infinito e tutto può essere sollevato più o meno. L'attuale "loop" qui sono le tre istruzioni da 0x27dccd1a 0x27dccdf. Il jmpnel ciclo è incondizionato (poiché il ciclo è infinito). Le uniche altre due istruzioni nel ciclo sono add rbc, 0x1- che è in aumento countOfIterations(nonostante il fatto che il ciclo non verrà mai chiuso, quindi questo valore non verrà letto: forse è necessario nel caso in cui vi entriate nel debugger), .. .
BeeOnRope

... e le testistruzioni dall'aspetto strano , che in realtà sono lì solo per l'accesso alla memoria (nota che eaxnon è nemmeno impostata nel metodo!): è una pagina speciale che è impostata per non essere letta quando la JVM vuole attivare tutti i thread per raggiungere un safepoint, quindi può fare gc o qualche altra operazione che richiede che tutti i thread siano in uno stato noto.
BeeOnRope,

Più precisamente, la JVM ha completamente sollevato il instance. a != instance.aconfronto fuori dal loop e lo esegue solo una volta, prima che il loop venga inserito! Sa che non è necessario ricaricare instanceo apoiché non sono dichiarati volatili e non esiste altro codice che possa modificarli sullo stesso thread, quindi presuppone che siano gli stessi durante l'intero ciclo, che è consentito dalla memoria modello.
BeeOnRope,

5

No, a != anon è thread-safe. Questa espressione è composta da tre parti: carica a, carica di anuovo ed esegui !=. È possibile per un altro thread ottenere il blocco intrinseco sul agenitore e modificare il valore atra le 2 operazioni di caricamento.

Un altro fattore è se aè locale. Se aè locale, nessun altro thread dovrebbe avervi accesso e quindi dovrebbe essere sicuro per i thread.

void method () {
    int a = 0;
    System.out.println(a != a);
}

dovrebbe anche sempre stampare false.

Dichiarare acome volatilenon risolverebbe il problema per if ais statico instance. Il problema non è che i thread hanno valori diversi di a, ma che un thread si carica adue volte con valori diversi. Si può effettivamente fare il caso meno thread-safe .. Se anon viene volatilequindi apuò essere memorizzato nella cache e un cambiamento in un altro thread non influenzerà il valore memorizzato nella cache.


Il tuo esempio synchronizedè sbagliato: per garantire la stampa di quel codice false, anche tutti i metodi impostati a dovrebbero essere synchronized.
Ruakh,

Perchè così? Se il metodo è sincronizzato come farebbe qualsiasi altro thread a ottenere il blocco intrinseco sul agenitore mentre il metodo è in esecuzione, necessario per impostare il valore a.
DoubleMx2,

1
Le tue premesse sono sbagliate. È possibile impostare il campo di un oggetto senza acquisire il suo blocco intrinseco. Java non richiede che un thread acquisisca il blocco intrinseco di un oggetto prima di impostare i suoi campi.
Ruakh,

3

Per quanto riguarda lo strano comportamento:

Dal momento che la variabile anon è contrassegnata come volatile, ad un certo punto il valore apotrebbe essere memorizzato nella cache dal thread. Entrambe le as di a != asono quindi la versione cache e quindi sempre lo stesso (significato flagè ora sempre false).


0

Anche la lettura semplice non è atomica. Se aè longe non contrassegnato come volatileallora su JVM a 32 bit long b = anon è thread-safe.


volatile e atomicità non sono correlate. anche se
segnerò

L'assegnazione di un campo lungo volatile è sempre atomica. Le altre operazioni come ++ non lo sono.
Zheka Kozlov,
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.