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