Как продемонстрировать проблемы переупорядочения инструкций Java?

При переупорядочении инструкций Java порядок выполнения кода изменяется JVM во время компиляции или времени выполнения, что может привести к тому, что несвязанные операторы будут выполняться не по порядку.

Поэтому мой вопрос:

Может ли кто-нибудь предоставить пример Java-программы/фрагмента, который надежно показывает проблему переупорядочения команд, которая не вызвана другими проблемами синхронизации (такими как кеширование/видимость или неатомный r/w, как в моей неудачной попытке такой демонстрации в моем предыдущем вопросе)

Чтобы подчеркнуть, я не ищу примеров теоретических вопросов переупорядочения. То, что я ищу, - это способ продемонстрировать их, увидев неправильные или неожиданные результаты запущенной программы.

Приведение в соответствие с примером ошибочного поведения, просто показывающее фактическое переупорядочение, происходящее в сборке простой программы, также может быть приятным.

Ответы

Ответ 1

Это демонстрирует переупорядочение определенных заданий, из 1M итераций обычно имеется несколько печатных строк.

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

}

Печать сборки для записи лямбда получает этот результат (среди прочих..)

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

Я не уверен, почему последний mov dword ptr [r12+r10*8+10h],1h не помечен putfield b и строкой 16, но вы можете увидеть замененное назначение b и c (c сразу после a).

EDIT: поскольку записи происходят в порядке, а, b, c и чтения происходят в обратном порядке c, b, a, вы никогда не должны видеть недопустимое состояние, если записи (или чтения) не упорядочиваются.

Записи, выполняемые одним процессором (или ядром), отображаются в том же порядке всеми процессорами, см., Например, этот ответ, который указывает на Руководство по системному программированию Intel Раздел 3 раздела 8.2.2.

Записи одного процессора наблюдаются в одном порядке всеми процессорами.

Ответ 2

Тестовое задание

Я написал тест JUnit 5, который проверяет, произошло ли переупорядочение команд после завершения двух потоков.

  • Тест должен пройти, если переупорядочение команды не произошло.
  • Тест должен завершиться неудачно, если произошло переупорядочение команды.

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

}

Результаты

Я проверил тест, пока он не сработал несколько раз. Результаты приведены ниже:

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.

объяснение

Ожидаемые результаты

  • x = 0, y = 1: threadA запускается до завершения, прежде threadB запускается threadB.
  • x = 1, y = 0: threadB запускается до завершения до threadA.
  • x = 1, y = 1: их команды чередуются.

Никто не может ожидать x = 0, y = 0, что может случиться, как показали результаты теста.

Действия в каждом потоке не имеют зависимости потока данных друг от друга и, соответственно, могут быть выполнены не по порядку. (Даже если они выполняются по порядку, время, с помощью которого кеши могут быть сброшены в основную память, может заставить с точки зрения threadB, что назначения в threadA произошли в обратном порядке.)

enter image description here Java Concurrency на практике, Брайан Гетц

Ответ 3

Для однопоточных исполнений переупорядочение не является проблемой вообще, из-за модели памяти Java (JMM) (гарантируют, что любые операции чтения, связанные с записью, являются полными упорядоченными) и не могут привести к неожиданным результатам.

Для параллельного выполнения правила совершенно разные, и все становится понятнее (даже при условии простого примера, который вызовет еще больше вопросов). Но даже это полностью описано JMM со всеми угловыми случаями, поэтому неожиданные результаты также запрещены. Как правило, запрещено, если все барьеры установлены правильно.

Для лучшего понимания переупорядочения я настоятельно рекомендую эту тему с большим количеством примеров.