字节码指令与处理器操作之间的关系

时间:2012-11-15 16:04:54

标签: java x86 bytecode processor atomicity

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

3 个答案:

答案 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 &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++的获取和添加说明。

答案 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());
    }
}