web-dev-qa-db-de.com

Ist der! = Check Thread sicher?

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

Aber ist das Überprüfen der Referenz mit sich selbst ein threadsicherer Vorgang?

a != a //is this thread-safe

Ich habe versucht, dies zu programmieren und mehrere Threads zu verwenden, aber es ist nicht fehlgeschlagen. Ich glaube, ich konnte 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 Wert des Ausgabe-Flags, was bedeutet, dass die Referenz != Überprüfung schlägt bei derselben Referenz fehl. ABER nach einigen Iterationen wird die Ausgabe zu einem konstanten Wert false, und wenn Sie das Programm über einen langen Zeitraum ausführen, wird keine einzige true Ausgabe generiert.

Wie die Ausgabe nach einigen n (nicht festgelegten) Iterationen nahelegt, 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
140
Narendra Pathai

Bei fehlender Synchronisation dieser Code

Object a;

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

kann true erzeugen. Dies ist der Bytecode für test()

    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, lädt es das Feld a zweimal in lokale Variablen, es ist eine nicht-atomare Operation, wenn a zwischendurch durch einen anderen Thread-Vergleich geändert wurde, was möglicherweise false ergibt.

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

122

Ist die Prüfung a != a Threadsicher?

Wenn a mö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 glaube, ich konnte kein Rennen auf meiner Maschine simulieren.

Das hat nichts zu bedeuten! Das Problem ist, dass der Code kein Thread ist, wenn eine Ausführung, in der a von einem anderen Thread aktualisiert wird, von der JLS zugelassen wird -sicher. Die Tatsache, dass Sie die Racebedingung nicht mit einem bestimmten Testfall auf einem bestimmten Computer und einer bestimmten Java -Implementierung auslösen können, schließt das Auftreten unter anderen Umständen nicht aus.

Bedeutet dies, dass a! = A true zurückgeben könnte.

Ja, theoretisch unter bestimmten Umständen.

Alternativ könnte a != afalse zurückgeben, obwohl sich a gleichzeitig änderte.


In Bezug auf das "seltsame Verhalten":

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

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

  1. Das Programm wird geladen und die JVM startet die 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 Race-Bedingung.

  2. Nach einiger Zeit wird der Code vom JIT-Compiler kompiliert. Das JIT-Optimierungsprogramm stellt fest, dass zwei Ladevorgänge desselben Speichersteckplatzes (a) nahe beieinander liegen, und optimiert den zweiten davon. (In der Tat gibt es eine Chance, dass es den Test völlig weg optimiert ...)

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

Beachten Sie, dass dies alles im Einklang mit dem steht, was das JLS einer Implementierung von Java ermöglicht.


@kriss kommentierte so:

Dies sieht so aus, als ob dies das sein könnte, was C- oder C++ - Programmierer "Undefiniertes Verhalten" nennen (implementierungsabhängig). Scheint, als ob es in Java in Eckfällen wie diesem ein paar UB geben könnte.

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

Der Compiler darf Ladevorgänge kombinieren und neu anordnen und speichern (und andere Dinge tun), vorausgesetzt, der Endeffekt des Codes ist derselbe:

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

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

47
Stephen C

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 Fehlschläge bei 10 000 Aufrufen. Also NEIN , es ist NICHT threadsicher

27

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

Das Java VM kann:

  1. Lies zweimal "a", lege jedes auf den Stapel und dann und vergleiche die Ergebnisse
  2. Lies "a" nur einmal, lege es auf den Stapel, dupliziere es ("dup" -Anweisung) und führe 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 Java Laufzeit (insbesondere dem JIT-Compiler) ab. Sie kann sich sogar während der Laufzeit Ihres Programms ändern.

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

Einzelheiten zur Thread-Sicherheit finden Sie unter JSR 1 ( Java Memory Model ).

15

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

-XX:InlineSmallCode=0

Dies sollte verhindern, dass die JIT-Optimierung (auf dem Hotspot 7-Server) durchgeführt wird und Sie true für immer sehen (ich habe bei 2.000.000 angehalten, aber ich nehme an, dass sie danach fortgesetzt wird).

Zur Information ist unten der JIT'ed-Code angegeben. Um ehrlich zu sein, lese ich Assembly nicht fließend genug, um zu wissen, ob der Test tatsächlich durchgeführt wurde oder wo die beiden Lasten herkommen. (Zeile 26 ist der Test flag = a != a und 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::[email protected] (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::[email protected] (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::[email protected] (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::[email protected] (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::[email protected] (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::[email protected] (line 26)
  0x00000000027dcd01: int3   
6
assylias

Nein, a != a ist nicht threadsicher. Dieser Ausdruck besteht aus drei Teilen: load a, load a again und perform !=. Es ist möglich, dass ein anderer Thread die interne Sperre für das übergeordnete Element von a erhält und den Wert von a zwischen den beiden Ladeoperationen ändert.

Ein weiterer Faktor ist jedoch, ob a lokal ist. Wenn a 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 false ausgeben.

Das Deklarieren von a als volatile würde das Problem nicht lösen, wenn astatic oder eine Instanz ist. Das Problem ist nicht, dass Threads unterschiedliche Werte von a haben, sondern dass ein Thread a zweimal mit unterschiedlichen Werten lädt. Dies kann die Thread-Sicherheit beeinträchtigen. Wenn a nicht volatile ist, wird a möglicherweise zwischengespeichert und eine Änderung in einem anderen Thread wirkt sich nicht auf den zwischengespeicherten Thread aus Wert.

5
DoubleMx2

In Bezug auf das seltsame Verhalten:

Da die Variable a nicht als volatile markiert ist, wird der Wert von a möglicherweise vom Thread zwischengespeichert. Beide as von a != a sind dann die zwischengespeicherte Version und somit immer gleich (dh flag ist jetzt immer false).

3
Walter Laan

Auch einfaches Lesen ist nicht atomar. Wenn along ist und nicht als volatile markiert ist, dann auf 32-Bit-JVMs long b = a ist nicht threadsicher.

0
ZhekaKozlov