10

Java 规范保证原始变量赋值始终是原子的(期望long和 double types.

相反,与著名的增量操作相对应的Fetch-and-Addi++操作将是非原子的,因为会导致读-修改-写操作。

假设这段代码:

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)是错误的吗?考虑到它已经是原子的。

4

3 回答 3

5

即使 i++ 将转换为 X86 Fetch-And-Add 指令也不会改变任何内容,因为 Fetch-And-Add 指令中提到的内存是指 CPU 的本地内存寄存器,而不是设备/应用程序的通用内存. 在现代 CPU 上,此属性将扩展到 CPU 的本地内存缓存,甚至可以扩展到多核 CPU 的不同内核使用的各种缓存,但在多线程应用程序的情况下;绝对不能保证此分布将扩展到线程本身使用的内存副本。

很明显,在多线程应用程序中,如果一个变量可以被同时运行的不同线程修改,那么你必须使用系统提供的一些同步机制,不能依赖指令 i++ 占用一行 java代码是原子的。

于 2012-11-15T16:52:08.610 回答
4

考虑第二个问题

您暗示这i++将转换为不正确的 X86 Fetch-And-Add 指令。如果代码是由 JVM 编译和优化的,那可能是真的(必须检查 JVM 的源代码来确认),但是该代码也可以在解释模式下运行,其中fetchadd是分开的而不是同步的。

出于好奇,我检查了为此 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

如您所见,它不使用a++.

于 2012-11-15T16:19:05.460 回答
0

关于你的第一个问题:读和写是原子的,但读/写操作不是。我找不到关于原语的具体参考,但JLS #17.7对参考说了类似的话:

对引用的写入和读取始终是原子的,无论它们是作为 32 位还是 64 位值实现的。

因此,在您的情况下, iload 和 istore 都是原子的,但整个 (iload, istore) 操作不是。

[认为]根本不需要同步 i++ 操作是否有错?

关于您的第二个问题,下面的代码在我的 x86 机器上打印 982(而不是 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());
    }
}
于 2012-11-15T16:32:49.387 回答