Ist der Thread Thread! = Sicher?


140

Ich weiß, dass zusammengesetzte Operationen, wie sie i++nicht threadsicher sind, mehrere Operationen beinhalten.

Aber ist das Überprüfen der Referenz mit sich selbst eine thread-sichere Operation?

a != a //is this thread-safe

Ich habe versucht, dies zu programmieren und mehrere Threads zu verwenden, aber es ist nicht fehlgeschlagen. Ich konnte wohl kein Rennen auf meiner Maschine simulieren.

BEARBEITEN:

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();
    }

}

Dies ist das Programm, mit dem ich die Thread-Sicherheit teste.

Seltsames Verhalten

Wenn mein Programm zwischen einigen Iterationen startet, erhalte ich den Ausgabeflag-Wert, was bedeutet, dass die Referenzprüfung !=für dieselbe Referenz fehlschlägt. ABER nach einigen Iterationen wird die Ausgabe zu einem konstanten Wert, falseund wenn das Programm dann über einen langen Zeitraum ausgeführt wird, wird keine einzige trueAusgabe generiert .

Wie die Ausgabe nach einigen n (nicht festen) Iterationen andeutet, scheint die Ausgabe ein konstanter Wert zu sein und ändert sich nicht.

Ausgabe:

Für einige Iterationen:

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
Was meinst du in diesem Zusammenhang mit "thread-sicher"? Fragen Sie, ob garantiert immer falsch zurückgegeben wird?
JB Nizet

@JBNizet ja. Daran habe ich gedacht.
Narendra Pathai

5
In einem Single-Thread-Kontext wird nicht immer false zurückgegeben. Es könnte ein NaN sein ..
Harold

4
Mögliche Erklärung: Der Code wurde just-in-time kompiliert und der kompilierte Code lädt die Variable nur einmal. Dies wird erwartet.
Marko Topolnik

3
Das Drucken einzelner Ergebnisse ist eine schlechte Methode, um Rennen zu testen. Das Drucken (sowohl das Formatieren als auch das Schreiben der Ergebnisse) ist im Vergleich zu Ihrem Test relativ kostspielig (und manchmal blockiert Ihr Programm beim Schreiben, wenn die Bandbreite der Verbindung zum Terminal oder zum Terminal selbst langsam ist). Außerdem enthält IO häufig eigene Mutexe, die die Ausführungsreihenfolge Ihrer Threads beeinflussen (beachten Sie, dass sich Ihre einzelnen Zeilen 1234:trueniemals gegenseitig zerschlagen ). Ein Renntest benötigt eine engere innere Schleife. Drucken Sie am Ende eine Zusammenfassung aus (wie unten bei einem Unit-Test-Framework).
Ben Jackson

Antworten:


124

In Ermangelung einer Synchronisation dieser Code

Object a;

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

kann produzieren true. Dies ist der Bytecode fürtest()

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

Wie wir sehen können, wird das Feld azweimal in lokale Variablen geladen. Es handelt sich um eine nichtatomare Operation, wenn sie azwischendurch durch einen anderen Thread-Vergleich geändert wurde false.

Auch das Problem der Speichersichtbarkeit ist hier relevant. Es gibt keine Garantie dafür, dass Änderungen, adie von einem anderen Thread vorgenommen wurden, für den aktuellen Thread sichtbar sind.


22
Obwohl es sich um starke Beweise handelt, ist Bytecode eigentlich kein Beweis. Es muss auch irgendwo in der JLS sein ...
Marko Topolnik

10
@ Marko Ich stimme Ihrem Denken zu, aber nicht unbedingt Ihrer Schlussfolgerung. Für mich ist der obige Bytecode die offensichtliche / kanonische Art der Implementierung !=, bei der LHS und RHS getrennt geladen werden. Wenn das JLS also nichts Spezifisches über Optimierungen erwähnt, wenn LHS und RHS syntaktisch identisch sind, gilt die allgemeine Regel, dh azweimaliges Laden .
Andrzej Doyle

20
Unter der Annahme, dass der generierte Bytecode dem JLS entspricht, ist dies ein Beweis!
Proskor

6
@Adrian: Erstens: Selbst wenn diese Annahme ungültig ist, reicht die Existenz eines einzelnen Compilers, der als "wahr" ausgewertet werden kann, aus, um zu demonstrieren, dass er manchmal als "wahr" ausgewertet werden kann (selbst wenn die Spezifikation dies verboten hat - was es ist nicht). Zweitens: Java ist gut spezifiziert und die meisten Compiler stimmen eng damit überein. Es ist sinnvoll, sie in dieser Hinsicht als Referenz zu verwenden. Drittens: Sie verwenden den Begriff "JRE", aber ich denke nicht, dass er bedeutet, was Sie denken, dass er bedeutet. . .
Ruakh

2
@AlexanderTorstling - "Ich bin mir nicht sicher, ob dies ausreicht, um eine Optimierung mit nur einem Lesevorgang auszuschließen." Es reicht nicht aus. In der Tat ist in Abwesenheit der Synchronisation (und der zusätzlichen "passiert vor" Beziehungen, die auferlegt werden) die Optimierung gültig,
Stephen C

47

Ist der Scheck a != athreadsicher?

Wenn amöglicherweise von einem anderen Thread aktualisiert werden kann (ohne ordnungsgemäße Synchronisierung!), Dann Nein.

Ich habe versucht, dies zu programmieren und mehrere Threads zu verwenden, bin aber nicht gescheitert. Ich konnte wohl kein Rennen auf meiner Maschine simulieren.

Das hat nichts zu bedeuten! Das Problem ist, dass der Code nicht threadsicher ist, wenn eine Ausführung, die avon einem anderen Thread aktualisiert wird , vom JLS zugelassen wird. Die Tatsache, dass Sie nicht veranlassen können, dass die Race-Bedingung mit einem bestimmten Testfall auf einem bestimmten Computer und einer bestimmten Java-Implementierung auftritt, schließt dies unter anderen Umständen nicht aus.

Bedeutet dies , dass a! = A zurückkehren konnte true.

Ja, theoretisch unter bestimmten Umständen.

Alternativ a != akönnte zurückkehren false, obwohl asich gleichzeitig ändert.


In Bezug auf das "seltsame Verhalten":

Wenn mein Programm zwischen einigen Iterationen startet, erhalte ich den Ausgabeflag-Wert, was bedeutet, dass die Referenz! = Prüfung für dieselbe Referenz fehlschlägt. ABER nach einigen Iterationen wird die Ausgabe zu einem konstanten Wert false, und wenn das Programm dann über einen langen Zeitraum ausgeführt wird, wird keine einzige echte Ausgabe generiert.

Dieses "seltsame" Verhalten stimmt mit dem folgenden Ausführungsszenario überein:

  1. Das Programm wird geladen und die JVM beginnt mit der Interpretation der Bytecodes. Da (wie wir aus der Javap-Ausgabe gesehen haben) der Bytecode zwei Ladevorgänge ausführt, sehen Sie (anscheinend) gelegentlich die Ergebnisse der Rennbedingung.

  2. Nach einiger Zeit wird der Code vom JIT-Compiler kompiliert. Der JIT-Optimierer stellt fest, dass zwei Ladungen desselben Speichersteckplatzes ( a) nahe beieinander liegen, und optimiert den zweiten entfernt. (Tatsächlich besteht die Möglichkeit, dass der Test vollständig optimiert wird ...)

  3. Jetzt manifestiert sich der Rennzustand nicht mehr, weil es keine zwei Lasten mehr gibt.

Beachten Sie, dass dies alles mit dem übereinstimmt, was das JLS einer Implementierung von Java ermöglicht.


@kriss kommentierte also:

Dies könnte das sein, was C- oder C ++ - Programmierer als "undefiniertes Verhalten" bezeichnen (implementierungsabhängig). Scheint, als ob es in Java-Fällen wie diesem ein paar UB in Java geben könnte.

Das Java-Speichermodell (in JLS 17.4 angegeben) gibt eine Reihe von Voraussetzungen an, unter denen ein Thread garantiert Speicherwerte sieht, die von einem anderen Thread geschrieben wurden. Wenn ein Thread versucht, eine von einem anderen geschriebene Variable zu lesen, und diese Voraussetzungen nicht erfüllt sind, kann es eine Reihe möglicher Ausführungen geben, von denen einige wahrscheinlich falsch sind (aus Sicht der Anforderungen der Anwendung). Mit anderen Worten, die Menge möglicher Verhaltensweisen (dh die Menge "wohlgeformter Ausführungen") ist definiert, aber wir können nicht sagen, welches dieser Verhaltensweisen auftreten wird.

Der Compiler kann Lasten kombinieren und neu anordnen und speichern (und andere Dinge tun), vorausgesetzt, der Endeffekt des Codes ist der gleiche:

  • wenn von einem einzelnen Thread ausgeführt, und
  • bei Ausführung durch verschiedene Threads, die korrekt synchronisiert werden (gemäß Speichermodell).

Wenn der Code jedoch nicht richtig synchronisiert wird (und daher die "Vorher" -Beziehungen die Menge der wohlgeformten Ausführungen nicht ausreichend einschränken), kann der Compiler Lasten und Speichern auf eine Weise neu anordnen, die "falsche" Ergebnisse liefert. (Aber das heißt wirklich nur, dass das Programm falsch ist.)


Bedeutet dies, dass a != adies wahr sein könnte?
Proskor

Ich meinte, dass ich auf meinem Computer möglicherweise nicht simulieren konnte, dass der obige Code nicht threadsicher ist. Vielleicht steckt dahinter eine theoretische Begründung.
Narendra Pathai

@ NarendraPathai - Es gibt keinen theoretischen Grund, warum Sie es nicht demonstrieren können. Es gibt möglicherweise einen praktischen Grund ... oder Sie haben einfach kein Glück gehabt.
Stephen C

Bitte überprüfen Sie meine aktualisierte Antwort mit dem Programm, das ich verwende. Die Prüfung gibt manchmal true zurück, aber es scheint ein seltsames Verhalten in der Ausgabe zu geben.
Narendra Pathai

1
@ NarendraPathai - Siehe meine Erklärung.
Stephen C

27

Bewiesen mit 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);
  }

}

Ich habe 2 Fehler bei 10 000 Aufrufen. Also NEIN , es ist NICHT threadsicher


6
Sie prüfen nicht einmal die Gleichheit ... der Random.nextInt()Teil ist überflüssig. Sie hätten new Object()genauso gut testen können .
Marko Topolnik

@MarkoTopolnik Bitte überprüfen Sie meine aktualisierte Antwort mit dem Programm, das ich verwende. Die Prüfung gibt manchmal true zurück, aber es scheint ein seltsames Verhalten in der Ausgabe zu geben.
Narendra Pathai

1
Randnotiz: Zufällige Objekte sollen normalerweise wiederverwendet werden und nicht jedes Mal erstellt werden, wenn Sie einen neuen Int benötigen.
Simon Forsberg

15

Nein ist es nicht. Für einen Vergleich muss die Java-VM die beiden zu vergleichenden Werte auf den Stapel legen und die Vergleichsanweisung ausführen (die vom Typ "a" abhängt).

Die Java-VM kann:

  1. Lesen Sie zweimal "a", legen Sie jedes auf den Stapel und vergleichen Sie die Ergebnisse
  2. Lesen Sie "a" nur einmal, legen Sie es auf den Stapel, duplizieren Sie es (Anweisung "dup") und führen Sie den Vergleich aus
  3. Beseitigen Sie den Ausdruck vollständig und ersetzen Sie ihn durch false

Im ersten Fall könnte ein anderer Thread den Wert für "a" zwischen den beiden Lesevorgängen ändern.

Welche Strategie gewählt wird, hängt vom Java-Compiler und der Java-Laufzeit ab (insbesondere vom JIT-Compiler). Es kann sich sogar zur Laufzeit Ihres Programms ändern.

Wenn Sie sicherstellen möchten, wie auf die Variable zugegriffen wird, müssen Sie sie erstellen volatile(eine sogenannte "halbe Speicherbarriere") oder eine vollständige Speicherbarriere hinzufügen ( synchronized). Sie können auch eine API auf höherer Ebene verwenden (z. B. AtomicIntegerwie von Juned Ahasan erwähnt).

Weitere Informationen zur Thread-Sicherheit finden Sie in JSR 133 ( Java Memory Model ).


Die Erklärung aals volatilewürde immer noch zwei unterschiedliche Lesevorgänge implizieren, mit der Möglichkeit eines Wechsels dazwischen.
Holger

6

Stephen C. hat alles gut erklärt. Zum Spaß können Sie versuchen, denselben Code mit den folgenden JVM-Parametern auszuführen:

-XX:InlineSmallCode=0

Dies sollte die Optimierung durch die JIT verhindern (dies geschieht auf dem Hotspot 7-Server) und Sie werden es truefür immer sehen (ich habe bei 2.000.000 angehalten, aber ich nehme an, dass es danach fortgesetzt wird).

Zur Information finden Sie unten den JIT-Code. Um ehrlich zu sein, lese ich die Montage nicht fließend genug, um zu wissen, ob der Test tatsächlich durchgeführt wird oder woher die beiden Lasten stammen. (Zeile 26 ist der Test flag = a != aund Zeile 31 ist die schließende Klammer von 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
Dies ist ein gutes Beispiel für die Art von Code, die JVM tatsächlich erzeugt, wenn Sie eine Endlosschleife haben und alles mehr oder weniger herausgezogen werden kann. Die eigentliche "Schleife" hier sind die drei Anweisungen von 0x27dccd1bis 0x27dccdf. Das jmpin der Schleife ist bedingungslos (da die Schleife unendlich ist). Die einzigen anderen zwei Anweisungen in der Schleife sind add rbc, 0x1- was inkrementell ist countOfIterations(trotz der Tatsache, dass die Schleife niemals verlassen wird, so dass dieser Wert nicht gelesen wird: Vielleicht wird er benötigt, falls Sie im Debugger in ihn einbrechen), .. .
BeeOnRope

... und die seltsam aussehende testAnweisung, die eigentlich nur für den Speicherzugriff vorhanden ist (Hinweis, der eaxin der Methode niemals festgelegt wird!): Es handelt sich um eine spezielle Seite, die auf nicht lesbar gesetzt ist, wenn die JVM alle Threads auslösen möchte Um einen Sicherheitspunkt zu erreichen, kann gc oder eine andere Operation ausgeführt werden, bei der alle Threads in einem bekannten Zustand sein müssen.
BeeOnRope

Genauer gesagt, die JVM hat den instance. a != instance.aVergleich vollständig aus der Schleife gehoben und führt ihn nur einmal durch, bevor die Schleife betreten wird! Es ist bekannt, dass es nicht erforderlich ist, neu zu laden, instanceoder ada sie nicht als flüchtig deklariert sind und es keinen anderen Code gibt, der sie im selben Thread ändern kann. Daher wird lediglich davon ausgegangen, dass sie während der gesamten Schleife, die vom Speicher zugelassen wird, gleich sind Modell.
BeeOnRope

5

Nein, a != aist nicht threadsicher. Dieser Ausdruck besteht aus drei Teilen: Laden a, aerneut laden und ausführen !=. Es ist möglich, dass ein anderer Thread die intrinsische Sperre für adas übergeordnete Element erhält und den Wert azwischen den beiden Ladevorgängen ändert .

Ein weiterer Faktor ist jedoch, ob aes sich um eine lokale handelt. Wenn aes lokal ist, sollten keine anderen Threads Zugriff darauf haben und daher threadsicher sein.

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

sollte auch immer drucken false.

Deklarieren aals volatilewürde das Problem für if ais staticoder instance nicht lösen . Das Problem ist nicht, dass Threads unterschiedliche Werte von haben a, sondern dass ein Thread azweimal mit unterschiedlichen Werten geladen wird. Dies kann dazu führen, dass der Fall weniger threadsicher ist. Wenn dies anicht der Fall ist, wird volatileer amöglicherweise zwischengespeichert, und eine Änderung in einem anderen Thread wirkt sich nicht auf den zwischengespeicherten Wert aus.


Ihr Beispiel mit synchronizedfalsch ist : für das Code drucken garantiert werden false, sind alle Methoden , die Set a sein müssten synchronized, auch.
Ruakh

Warum so? Wenn die Methode synchronisiert ist, wie würde ein anderer Thread die intrinsische Sperre für adas übergeordnete Element erhalten, während die Methode ausgeführt wird, die zum Festlegen des Werts erforderlich ist a.
DoubleMx2

1
Ihre Räumlichkeiten sind falsch. Sie können das Feld eines Objekts festlegen, ohne dessen intrinsische Sperre zu erhalten. Java benötigt keinen Thread, um die intrinsische Sperre eines Objekts zu erhalten, bevor seine Felder festgelegt werden.
Ruakh

3

In Bezug auf das seltsame Verhalten:

Da die Variable anicht als markiert ist volatile, wird der Wert von amöglicherweise irgendwann vom Thread zwischengespeichert. Beide as a != asind dann die zwischengespeicherte Version und somit immer gleich (Bedeutung flagist jetzt immer false).


0

Selbst einfaches Lesen ist nicht atomar. Wenn aist longund nicht als volatiledann markiert, ist bei 32-Bit-JVMs long b = anicht threadsicher.


flüchtig und Atomizität sind nicht miteinander verbunden. Selbst wenn ich eine flüchtige Markierung markiere, wird sie nicht atomar sein
Narendra Pathai

Die Zuordnung eines flüchtigen Langfeldes ist immer atomar. Die anderen Operationen wie ++ sind nicht.
ZhekaKozlov
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.