Java规范保证原始变量赋值始终是原子的(期望long
和双types
。
相反,对应于着名的i++
递增操作的Fetch-and-Add操作将是非原子操作,因为这会导致读 - 修改 - 写操作。
假设这段代码:
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依赖于分配操作是一个始终以原子方式执行的操作,无论处理器的体系结构如何那么可以确保其规范中的永久原子性(对于原始赋值)吗?
第二个问题:使用非常现代的X86处理器确认并且不跨不同架构共享编译代码是不对的,根本不需要同步i++
操作(或{ {1}})?考虑到它已经是原子的。
答案 0 :(得分:5)
即使i ++转换为X86 Fetch-And-Add指令也不会改变任何内容,因为Fetch-And-Add指令中提到的内存是指CPU的本地内存注册,而不是指向CPU的本地内存注册。设备/应用。在现代CPU上,此属性将扩展到CPU的本地内存缓存,甚至可以扩展到多核CPU的不同内核使用的各种缓存,但是在多线程应用程序的情况下;绝对没有保证这个发行版将扩展到线程本身使用的内存副本。
很明显,在多线程应用程序中,如果一个变量可以被同时运行的不同线程修改,那么你必须使用系统提供的一些同步机制,你不能依赖于i ++指令占用一个单独的事实。 java代码行是原子的。
答案 1 :(得分:4)
考虑第二个问题。
你暗示i++
将转换为X86 Fetch-And-Add指令,这是不正确的。如果代码由JVM编译和优化,它可能为真(必须检查JVM的源代码以确认),但该代码也可以在解释模式下运行,其中和 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 <- 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
如您所见,它不使用a++
的获取和添加说明。
答案 2 :(得分:1)
关于第一个问题:读取和写入是原子的,但读取/写入操作不是。我找不到关于原语的具体参考,但JLS #17.7说了类似的关于引用的内容:
对引用的写入和读取始终是原子的,无论它们是实现为32位还是64位值。
所以在你的情况下,iload和istore都是原子的,但整个(iload,istore)操作不是。
[考虑]是否根本不需要同步i ++操作是不对的?
关于你的第二个问题,下面的代码在我的x86机器上打印982(而不是1,000),这表明某些++
在翻译中丢失==&gt;即使在支持读取和添加指令的处理器体系结构上,您也需要正确同步++
操作。
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());
}
}