web-dev-qa-db-fra.com

Comment démontrer des problèmes de réorganisation d'instructions Java?

Avec une instruction Java réordonnant, l'ordre d'exécution du code est modifié par la machine virtuelle Java au moment de la compilation ou de l'exécution, ce qui peut éventuellement provoquer l'exécution d'instructions non liées dans le désordre.

Donc ma question est:

Quelqu'un peut-il fournir un exemple de programme/extrait Java, qui indique de manière fiable un problème de réorganisation des instructions, qui n'est pas causé également par d'autres problèmes de synchronisation (tels que la mise en cache/visibilité ou la lecture non atomique, comme lors de ma tentative infructueuse de démonstration) dans ma question précédente )

Pour souligner, je ne cherche pas d'exemples de problèmes de réorganisation théorique. Ce que je recherche, c’est un moyen de les démontrer en constatant les résultats incorrects ou inattendus d’un programme en cours.

Si ce n’est un exemple de comportement fautif, le simple fait d’afficher les commandes en cours dans l’assemblage d’un programme simple pourrait également être intéressant. 

37
Gonen I

Cela montre la réorganisation de certaines affectations. Sur 1 million d'itérations, il y a généralement quelques lignes imprimées.

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

}

Imprimer l’Assemblée pour l’écriture lambda obtient cette sortie (entre autres ..)

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

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

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

  0x00007f73b51a0118: 43c744d41001000000  mov       dword ptr [r12+r10*8+10h],1h
                                                ;*synchronization entry
                                                ; - Java.lang.Thread::run@-1 (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

Je ne suis pas sûr de savoir pourquoi le dernier mov dword ptr [r12+r10*8+10h],1h n'est pas marqué avec putfield b et la ligne 16, mais vous pouvez voir l'affectation échangée de b et c (c juste après a).

EDIT: Parce que les écritures se déroulent dans l'ordre a, b, c et que les lectures se déroulent dans l'ordre inverse c, b, a, vous ne devriez jamais voir un état invalide à moins que les écritures (ou les lectures) ne soient réorganisées. 

Les écritures effectuées par un seul processeur (ou cœur) sont visibles dans le même ordre par tous les processeurs, voir par exemple. cette réponse , qui pointe vers Guide de programmation système Intel Volume 3 section 8.2.2.

Les écritures effectuées par un seul processeur sont observées dans le même ordre par tous les processeurs.

5
frant.hartm

Tester

J'ai écrit un JUnit 5 test qui vérifie si le réordonnancement des instructions a eu lieu après la fin de deux threads.

  • Le test doit réussir si aucune réorganisation des instructions n’est arrivée.
  • Le test doit échouer si la réorganisation des instructions a eu lieu. 

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

}

Résultats

J'ai exécuté le test jusqu'à ce qu'il échoue plusieurs fois. Les résultats sont les suivants:

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.

Explication

Les résultats attendus sont

  • x = 0, y = 1: threadA s'achève avant le début de threadB.
  • x = 1, y = 0: threadB s'achève avant le début de threadA.
  • x = 1, y = 1: leurs instructions sont entrelacées.

Personne ne peut s’attendre à x = 0, y = 0, ce qui peut arriver comme le montrent les résultats du test.

Les actions de chaque thread ne dépendent pas l'une de l'autre du flux de données et peuvent donc être exécutées dans le désordre. (Même s'ils sont exécutés dans l'ordre, le moment auquel les caches sont vidés dans la mémoire principale peut faire apparaître, du point de vue de threadB, que les assignations dans threadA se sont déroulées dans l'ordre inverse.)

 enter image description here Concurrence Java en pratique, Brian Goetz

2
Andrew Tobilko

Pour les exécutions à un seul thread, le réordonnancement n'est pas du tout un problème, en raison de JMM (Java Memory Model) (garantit que toutes les actions de lecture liées aux écritures sont totalement ordonnées) et ne peuvent pas donner lieu à des résultats inattendus.

Pour une exécution simultanée, les règles sont complètement différentes et les choses deviennent plus compliquées à comprendre (même en fournissant un exemple simple qui soulèvera encore plus de questions). Mais même cela est totalement décrit par JMM avec tous les cas de figure, donc, résultats inattendus également interdits. En règle générale, interdit si toutes les barrières sont correctement placées.

Pour une meilleure compréhension des commandes, je recommande fortement this / sujet avec beaucoup d'exemples à l'intérieur.

0
user3904219