【问题标题】:Relation between bytecode instructions and processor operations字节码指令和处理器操作之间的关系
【发布时间】:2012-11-15 16:04:53
【问题描述】:

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 处理器可以(至少是现代处理器)可以原子地操作增量操作,如前所述:

在计算机科学中,取并加 CPU 指令是一种特殊的 以原子方式修改内存内容的指令 地点。用于实现互斥和并发 多处理器系统中的算法,信号量的推广。

因此,第一个问题:尽管字节码需要两个步骤(加载和存储),Java 是否依赖于这样一个事实,即无论处理器的架构如何,赋值操作都是原子执行的操作因此可以确保其规范中的永久原子性(对于原始分配)?

第二个问题:确认使用非常现代的 X86 处理器并且没有跨不同架构共享编译代码,根本不需要同步 i++ 操作(或 AtomicInteger )?考虑到它已经是原子的了。

【问题讨论】:

  • 我的理解是,赋值的原子性只意味着 istore 是原子的——换句话说,在a = b 中,有可能读取b,然后突变为一个新值,然后原始值分配给a。但是,原子性保证a 不会是b 拥有的两个值的混合。
  • @assylias 这就是我的想法,我同意这个观点:)

标签: java x86 bytecode processor atomic


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    考虑第二个问题

    您暗示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++ 的 Fetch-And-Add 指令。

    【讨论】:

    • +1 如果规范不保证原子操作并不意味着不会使用原子操作。顺便说一句:在上面的示例中,我假设 JIT 会将方法优化到零。 ;)
    【解决方案3】:

    关于你的第一个问题:读和写是原子的,但读/写操作不是。我找不到关于原语的具体参考,但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());
        }
    }
    

    【讨论】:

    • 这证实了 ShyJ 所声称的 =>“您暗示 i++ 将转换为不正确的 X86 Fetch-And-Add 指令”。
    • @Mik378 在您的第一个问题上添加了一些内容。
    猜你喜欢
    • 2015-02-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-17
    • 2011-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多