Связь между инструкциями байткода и операциями процессора
Спецификация 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 <- 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());
}
}