關係

2012-11-15 41 views
10

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指令是一種特殊的指令,其原子上修改s的內存 的位置。它用於在多處理器系統中實現互斥和併發算法,這是信號量的泛化。

因此,第一個問題:儘管字節碼需要兩個步驟(加載和存儲)的事實,確實Java的依賴於一個事實,即賦值操作總是進行原子無論處理器的架構,因此操作可以確保其規範中的永久原子性(用於原始分配)?

第二個問題:這是錯誤的確認與極具現代感的X86處理器,沒有跨不同體系結構共享編譯的代碼,就沒有必要在所有同步i++操作(或AtomicInteger)?考慮到它已經是原子。

+1

我的理解是,assignement的原子性只意味着istore是原子的 - 換句話說,在'a = b'中,有可能'b'被讀取,然後突變爲一個新值,然後原始值是分配給'a'。但是,原子性保證'a'不會是'b'所持有的2個值的混合。 – assylias

+0

@assylias這就是我的想法,我同意這個觀點:) – Mik378

回答

4

考慮到第二個問題

您意味着i++將轉化爲X86取指加指令,這是不正確的。如果代碼被編譯和優化的JVM它可能是真實的(會檢查JVM的源代碼,以確認),但代碼也可以解釋模式,其中添加運行是分開的而不是同步的。

出於好奇,我檢查了這個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++的提取和添加說明。

+0

+1如果規範不保證原子操作並不意味着不會使用。順便說一句:在上面的例子中,我會假設JIT會優化方法到沒有。 ;) –

+0

@ShyJ大樣本,謝謝! – Mik378

1

關於您的第一個問題:讀取和寫入是原子性的,但讀取/寫入操作不是。我無法找到原語特定的參考,但JLS #17.7說類似的話就引用:

寫入和引用的讀取總是原子,無論它們是否被實現爲32位或64位值。

所以在你的情況下,iload和istore都是原子的,但整個(iload,istore)操作不是。

[認爲]根本不需要同步i ++操作嗎?

關於你提到的第二個問題,下面的代碼打印982我的x86機器(而不是1000),這表明一些++翻譯迷路==>你需要甚至在處理器架構正確同步++操作上支持讀取和添加指令。

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()); 
    } 
} 
+1

這證實了什麼ShyJ聲稱=>「你暗示我++會轉化爲X86取和加指令,這是不正確的。 – Mik378

+0

@ Mik378在你的第一個問題上增加了一些內容。 – assylias

+0

謝謝,現在很清楚:) – Mik378

5

即使我++將轉化爲一個X86讀取和添加指令會改變什麼,因爲在讀取和添加指令mentionned內存指的是CPU的本地內存registres,而不是到設備/應用程序的一般記憶。在現代CPU上,此屬性將擴展到CPU的本地內存緩存,甚至可擴展到多核CPU的不同內核使用的各種緩存,但在多線程應用程序的情況下;這個發行版本將不會延伸到線程本身使用的內存副本。

很明顯,在一個多線程應用程序中,如果一個變量可以被同時運行的不同線程修改,那麼你必須使用系統提供的一些同步機制,並且你不能依賴指令i ++佔用單行的java代碼是原子的。