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

спецификации Java гарантирует примитивного присвоения переменных всегда атомарны (за long и дважды types.

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

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

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 может (по крайней мере, современные), работает операция приращения атомарно, как сказано:

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

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

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

3 ответов


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

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

из любопытства я проверено, какой код сборки генерируется для этого кода 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++;
  }
}

Я 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 версия JVM (этот у меня был где-то на моем диске).

это решающий результат его запуска (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++.


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

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


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

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

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

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

Что касается вашего второго вопроса, код ниже печатает 982 на моей машине x86 (а не 1,000), который показывает, что некоторые ++ заблудился в переводе ==> вам нужно правильно синхронизировать ++ работа даже на архитектуре процессора, которая поддерживает инструкцию 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());
    }
}