Связь между инструкциями байткода и операциями процессора

Спецификация Java гарантирует, что примитивные назначения переменных всегда являются атомарными (ожидайте для long и double types.

Наоборот, операция Fetch-and-Add, соответствующая известной операции приращения i++, была бы неатомной, потому что это привело к чтению -modify-write.

Предполагая этот код:

public void assign(int b) {
    int a = b;
}

Сгенерированный байт-код:

public void assign(int);
    Code:
       0: iload_1       
       1: istore_2      
       2: return 

Таким образом, мы видим, что назначение состоит из шагов два (загрузка и хранение).

Предполагая этот код:

public void assign(int b) {
        int i = b++;
}

Байт-код:

public void assign(int);
    Code:
       0: iload_1       
       1: iinc          1, 1    //extra step here regarding the previous sample
       4: istore_2      
       5: return 

Зная, что процессор X86 может (по крайней мере, современные) работать атомизированным образом, как сказано:

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

Таким образом, первый вопрос: Несмотря на то, что для байт-кода требуются оба этапа (загрузка и хранение), делает ли Java о том, что операция присваивания является операцией, выполняемой атомарно независимо от архитектуры процессора и поэтому может обеспечить постоянную атомарность (для примитивных присвоений) в ее спецификации?

Второй вопрос: Неправильно ли это утверждать, что с очень современным процессором X86 и без совместного использования скомпилированного кода для разных архитектур нет необходимости синхронизировать операцию i++ (или AtomicInteger)? Считая это уже атомарным.

Ответ 1

Учитывая Второй вопрос.

Вы подразумеваете, что i++ будет переводиться в инструкцию X86 Fetch-And-Add, которая неверна. Если код скомпилирован и оптимизирован JVM, он может быть правдой (нужно было бы проверить исходный код JVM, чтобы подтвердить это), но этот код также может работать в интерпретируемом режиме, где выборка и добавление разделяются и не синхронизируются.

Из любопытства я проверил, какой код сборки сгенерирован для этого Java-кода:

public class Main {
    volatile int a;

  static public final void main (String[] args) throws Exception {
    new Main ().run ();
  }

  private void run () {
      for (int i = 0; i < 1000000; i++) {
        increase ();
      }  
  } 

  private void increase () {
    a++;
  }
}

Я использовал версию JVM Java HotSpot(TM) Server VM (17.0-b12-fastdebug) for windows-x86 JRE (1.6.0_20-ea-fastdebug-b02), built on Apr 1 2010 03:25:33 (этот был у меня на моем диске).

Это решающий результат его запуска (java -server -XX:+PrintAssembly -cp . Main):

Сначала он скомпилирован в это:

00c     PUSHL  EBP
    SUB    ESP,8    # Create frame
013     MOV    EBX,[ECX + #8]   # int ! Field  VolatileMain.a
016     MEMBAR-acquire ! (empty encoding)
016     MEMBAR-release ! (empty encoding)
016     INC    EBX
017     MOV    [ECX + #8],EBX ! Field  VolatileMain.a
01a     MEMBAR-volatile (unnecessary so empty encoding)
01a     LOCK ADDL [ESP + #0], 0 ! membar_volatile
01f     ADD    ESP,8    # Destroy frame
    POPL   EBP
    TEST   PollPage,EAX ! Poll Safepoint

029     RET

Затем он встроен и скомпилирован в это:

0a8   B11: #    B11 B12 &lt;- B10 B11   Loop: B11-B11 inner stride: not constant post of N161 Freq: 0.999997
0a8     MOV    EBX,[ESI]    # int ! Field  VolatileMain.a
0aa     MEMBAR-acquire ! (empty encoding)
0aa     MEMBAR-release ! (empty encoding)
0aa     INC    EDI
0ab     INC    EBX
0ac     MOV    [ESI],EBX ! Field  VolatileMain.a
0ae     MEMBAR-volatile (unnecessary so empty encoding)
0ae     LOCK ADDL [ESP + #0], 0 ! membar_volatile
0b3     CMP    EDI,#1000000
0b9     Jl,s  B11   # Loop end  P=0.500000 C=126282.000000

Как вы можете видеть, он не использует инструкции Fetch-And-Add для a++.

Ответ 2

Даже если я ++ переводится в инструкцию X86 Fetch-And-Add, ничего не изменит, потому что память, указанная в инструкции Fetch-And-Add, относится к локальным регистрам памяти процессора, а не к общей памяти устройство/приложение. На современном процессоре это свойство будет распространяться на локальные кэши памяти центрального процессора и может даже распространяться на различные кеши, используемые разными ядрами для многоядерного процессора, но в случае многопоточного приложения; нет абсолютно никакой гарантии, что это распространение будет распространяться на копию памяти, используемой самими нитями.

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

Ответ 3

Что касается вашего первого вопроса: чтение и запись являются атомарными, но операция чтения/записи не является. Я не мог найти конкретную ссылку на примитивы, но JLS # 17.7 говорит что-то подобное в отношении ссылок:

Записи и чтения ссылок всегда являются атомарными, независимо от того, реализованы ли они как 32-битные или 64-битные значения.

Итак, в вашем случае, как iload, так и istore являются атомарными, но вся операция (iload, istore) отсутствует.

Неправильно ли [считать, что] нет необходимости вообще синхронизировать операцию я ++?

Что касается вашего второго вопроса, приведенный ниже код печатает 982 на моей машине x86 (а не 1000), которая показывает, что некоторая ++ потеряна в переводе == > вам необходимо правильно синхронизировать операцию ++ даже на процессоре архитектура, которая поддерживает инструкцию fetch-and-add.

public class Test1 {

    private static int i = 0;

    public static void main(String args[]) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        final CountDownLatch start = new CountDownLatch(1);
        final Set<Integer> set = new ConcurrentSkipListSet<>();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                try {
                    start.await();
                } catch (InterruptedException ignore) {}
                for (int j = 0; j < 100; j++) {
                    set.add(i++);
                }
            }
        };

        for (int j = 0; j < 10; j++) {
            executor.submit(r);
        }
        start.countDown();
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.SECONDS);
        System.out.println(set.size());
    }
}