webentwicklung-frage-antwort-db.com.de

Wie demonstrieren Sie Probleme bei der Neuordnung von Java-Anweisungen?

Bei der Neuanordnung von Java-Anweisungen wird die Ausführungsreihenfolge des Codes von der JVM zur Kompilierzeit oder zur Laufzeit geändert, was möglicherweise dazu führt, dass nicht zusammenhängende Anweisungen außerhalb der Reihenfolge ausgeführt werden.

Meine Frage ist also:

Kann jemand ein Beispiel für ein Java-Programm/Snippet angeben, das zuverlässig ein Problem bei der Neuordnung von Anweisungen zeigt, das nicht auch durch andere Synchronisierungsprobleme (z. B. Caching/Visibility oder nicht-atomares R/W) verursacht wird, wie in meinem fehlgeschlagenen Versuch einer solchen Demo in meine vorherige Frage )

Um das zu betonen, suche ich keine Beispiele für theoretische Umordnungsprobleme. Was ich suche, ist eine Möglichkeit, sie tatsächlich zu demonstrieren, indem falsche oder unerwartete Ergebnisse eines laufenden Programms angezeigt werden.

Abgesehen von einem fehlerhaften Verhaltensbeispiel könnte es auch Nizza sein, nur die tatsächliche Neuordnung in der Assembly eines einfachen Programms zu zeigen. 

37
Gonen I

Dies demonstriert die Neuordnung bestimmter Zuweisungen. In 1M-Iterationen befinden sich normalerweise einige gedruckte Zeilen.

public class App {

public static void main(String[] args) {

    for (int i = 0; i < 1000_000; i++) {
        final State state = new State();

        // a = 0, b = 0, c = 0

        // Write values
        new Thread(() -> {
            state.a = 1;
            // a = 1, b = 0, c = 0
            state.b = 1;
            // a = 1, b = 1, c = 0
            state.c = state.a + 1;
            // a = 1, b = 1, c = 2
        }).start();

        // Read values - this should never happen, right?
        new Thread(() -> {
            // copy in reverse order so if we see some invalid state we know this is caused by reordering and not by a race condition in reads/writes
            // we don't know if the reordered statements are the writes or reads (we will se it is writes later)
            int tmpC = state.c;
            int tmpB = state.b;
            int tmpA = state.a;

            if (tmpB == 1 && tmpA == 0) {
                System.out.println("Hey wtf!! b == 1 && a == 0");
            }
            if (tmpC == 2 && tmpB == 0) {
                System.out.println("Hey wtf!! c == 2 && b == 0");
            }
            if (tmpC == 2 && tmpA == 0) {
                System.out.println("Hey wtf!! c == 2 && a == 0");
            }
        }).start();

    }
    System.out.println("done");
}

static class State {
    int a = 0;
    int b = 0;
    int c = 0;
}

}

Das Drucken der Assembly für das Schreib-Lambda erhält diese Ausgabe (ua).

                                                ; {metadata('com/example/App$$Lambda$1')}
  0x00007f73b51a0100: 752b                jne       7f73b51a012dh
                                                ;*invokeinterface run
                                                ; - Java.lang.Thread::[email protected] (line 748)

  0x00007f73b51a0102: 458b530c            mov       r10d,dword ptr [r11+0ch]
                                                ;*getfield arg$1
                                                ; - com.example.App$$Lambda$1/1831932724::[email protected]
                                                ; - Java.lang.Thread::[email protected] (line 747)

  0x00007f73b51a0106: 43c744d41402000000  mov       dword ptr [r12+r10*8+14h],2h
                                                ;*putfield c
                                                ; - com.example.App::[email protected] (line 18)
                                                ; - com.example.App$$Lambda$1/1831932724::[email protected]
                                                ; - Java.lang.Thread::[email protected] (line 747)
                                                ; implicit exception: dispatches to 0x00007f73b51a01b5
  0x00007f73b51a010f: 43c744d40c01000000  mov       dword ptr [r12+r10*8+0ch],1h
                                                ;*putfield a
                                                ; - com.example.App::[email protected] (line 14)
                                                ; - com.example.App$$Lambda$1/1831932724::[email protected]
                                                ; - Java.lang.Thread::[email protected] (line 747)

  0x00007f73b51a0118: 43c744d41001000000  mov       dword ptr [r12+r10*8+10h],1h
                                                ;*synchronization entry
                                                ; - Java.lang.Thread::[email protected] (line 747)

  0x00007f73b51a0121: 4883c420            add       rsp,20h
  0x00007f73b51a0125: 5d                  pop       rbp
  0x00007f73b51a0126: 8505d41eb016        test      dword ptr [7f73cbca2000h],eax
                                                ;   {poll_return}
  0x00007f73b51a012c: c3                  ret
  0x00007f73b51a012d: 4181f885f900f8      cmp       r8d,0f800f985h

Ich bin nicht sicher, warum der letzte mov dword ptr [r12+r10*8+10h],1h nicht mit putfield b und Zeile 16 markiert ist, aber Sie können die vertauschte Zuordnung von b und c sehen (c direkt nach a).

EDIT: Da Schreibvorgänge in der Reihenfolge a, b, c und Lesevorgänge in umgekehrter Reihenfolge c, b ausgeführt werden, sollten Sie niemals einen ungültigen Status sehen, es sei denn, die Schreibvorgänge (oder Lesevorgänge) werden neu angeordnet. 

Schreibvorgänge, die von einer einzelnen CPU (oder einem Kern) ausgeführt werden, sind in der gleichen Reihenfolge von allen Prozessoren sichtbar, siehe z. diese Antwort , die auf Intel System Programmierhandbuch Band 3 Abschnitt 8.2.2 zeigt.

Schreibvorgänge von einem einzigen Prozessor werden von allen Prozessoren in der gleichen Reihenfolge beobachtet.

5
frant.hartm

Prüfung

Ich schrieb einen JUnit 5 Test, der prüft, ob die Befehlsumordnung nach dem Beenden von zwei Threads stattgefunden hat.

  • Der Test muss bestanden werden, wenn keine Neuordnung der Anweisungen stattgefunden hat.
  • Der Test muss fehlschlagen, wenn eine Neuordnung der Anweisungen erfolgt ist. 

public class InstructionReorderingTest {

    static int x, y, a, b;

    @org.junit.jupiter.api.BeforeEach
    public void init() {
        x = y = a = b = 0;
    }

    @org.junit.jupiter.api.Test
    public void test() throws InterruptedException {
        Thread threadA = new Thread(() -> {
            a = 1;
            x = b;
        });
        Thread threadB = new Thread(() -> {
            b = 1;
            y = a;
        });

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();

        org.junit.jupiter.api.Assertions.assertFalse(x == 0 && y == 0);
    }

}

Ergebnisse

Ich habe den Test bis es fehlschlägt mehrmals ausgeführt. Die Ergebnisse sind wie folgt:

InstructionReorderingTest.test [*] (12s 222ms): 29144 total, 1 failed, 29143 passed.
InstructionReorderingTest.test [*] (26s 678ms): 69513 total, 1 failed, 69512 passed.
InstructionReorderingTest.test [*] (12s 161ms): 27878 total, 1 failed, 27877 passed.

Erläuterung

Die Ergebnisse, die wir erwarten, sind

  • x = 0, y = 1: threadA wird vollständig ausgeführt, bevor threadB gestartet wird.
  • x = 1, y = 0: threadB wird vollständig ausgeführt, bevor threadA gestartet wird.
  • x = 1, y = 1: ihre Anweisungen sind verschachtelt.

Niemand kann x = 0, y = 0 erwarten, was passieren kann, wie die Testergebnisse zeigten.

Die Aktionen in jedem Thread haben keine voneinander abhängigen Datenflüsse und können dementsprechend außerhalb der Reihenfolge ausgeführt werden. (Selbst wenn sie in der Reihenfolge ausgeführt werden, kann der Zeitpunkt, zu dem die Caches in den Hauptspeicher geleert werden, aus der Sicht von threadB den Anschein erwecken, dass die Zuweisungen in threadA in umgekehrter Reihenfolge aufgetreten sind.)

 enter image description here Java Parallelität in der Praxis, Brian Goetz

2
Andrew Tobilko

Für Single-Thread-Ausführungen ist das Umordnen kein Problem, da Java Memory Model (JMM) (garantiert, dass alle Leseoperationen, die sich auf das Schreiben beziehen, insgesamt geordnet sind) und nicht zu unerwarteten Ergebnissen führen.

Für die gleichzeitige Ausführung sind Regeln völlig unterschiedlich und die Dinge werden schwieriger zu verstehen (selbst wenn sie ein einfaches Beispiel liefern, das noch mehr Fragen aufwirft). Aber auch dies wird von JMM mit allen Eckfällen vollständig beschrieben, so dass unerwartete Ergebnisse auch verboten sind. Generell verboten, wenn alle Barrieren richtig gestellt sind.

Zum besseren Verständnis der Nachbestellung empfehle ich dringend das dieses Thema mit vielen Beispielen.

0
user3904219