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

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

Наоборот, операция Выборка-и-Добавление соответствует знаменитой операции 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 может (по крайней мере, современные) выполнять операцию увеличения атомарно, как сказано:

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

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

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


person Mik378    schedule 15.11.2012    source источник
comment
Насколько я понимаю, атомарность присваивания означает только то, что istore является атомарным - другими словами, в a = b возможно, что b читается, а затем преобразуется в новое значение, после чего исходное значение присваивается a. Однако атомарность гарантирует, что a не будет смесью двух значений, хранящихся в b.   -  person assylias    schedule 15.11.2012
comment
@assylias Вот что я подумал, я согласен с этой точкой зрения :)   -  person Mik378    schedule 15.11.2012


Ответы (3)


Принимая во внимание второй вопрос.

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

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

person ShyJ    schedule 15.11.2012
comment
+1 Если спецификация не гарантирует атомарную операцию, это не значит, что она не будет использоваться. Кстати: в приведенном выше примере я бы предположил, что JIT оптимизирует метод до нуля. ;) - person Peter Lawrey; 15.11.2012

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

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

person SylvainL    schedule 15.11.2012

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

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

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

Разве неправильно [считать, что] вообще не нужно синхронизировать операцию i++?

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

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());
    }
}
person assylias    schedule 15.11.2012
comment
Это подтверждает то, что утверждает ShyJ => Вы подразумеваете, что i++ переведет в инструкцию X86 Fetch-And-Add, что неверно. - person Mik378; 15.11.2012
comment
@Mik378 добавил кое-что к вашему первому вопросу. - person assylias; 15.11.2012