10

Java 仕様では、プリミティブ変数の割り当てが常にアトミックであることを保証しています ( forlongと 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 

したがって、割り当ては2 つのステップ (読み込みと保存) で構成されていることがわかります。

このコードを仮定します:

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 は、代入操作がプロセッサのアーキテクチャに関係なく常にアトミックに実行される操作であり、永続的なアトミック性 (プリミティブ代入の場合) を保証できるという事実に依存していますか? ) その仕様で?

2 番目の質問:i++非常に最新の X86 プロセッサを使用し、コンパイルされたコードを異なるアーキテクチャ間で共有することなく、操作 (または)を同期する必要がまったくないことを確認するのは間違っていAtomicIntegerますか? それを考えると、すでにアトミックです。

4

3 に答える 3

5

i++ が X86 Fetch-And-Add 命令に変換されたとしても、Fetch-And-Add 命令で言及されているメモリは、デバイス/アプリケーションの一般的なメモリではなく、CPU のローカル メモリ レジスタを参照するため、何も変更されません。 . 最新の CPU では、このプロパティは CPU のローカル メモリ キャッシュに拡張され、マルチコア CPU のさまざまなコアで使用されるさまざまなキャッシュに拡張することもできますが、マルチスレッド アプリケーションの場合はそうではありません。この配布が、スレッド自体によって使用されるメモリのコピーに拡張されるという保証はまったくありません。

明らかに、マルチスレッド アプリケーションでは、変数が同時に実行されている異なるスレッドによって変更される可能性がある場合、システムによって提供される何らかの同期メカニズムを使用する必要があり、命令 i++ が Java の 1 行を占有するという事実に依存することはできません。コードをアトミ​​ックにします。

于 2012-11-15T16:52:08.610 に答える
4

2 番目の質問を検討します。

i++あなたは、それが真実ではない X86 Fetch-And-Add 命令に変換されることを暗示しています。コードが 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++;
  }
}

JVMのバージョンを使用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しました(これはドライブのどこかにありました)。

これらは、それを実行した場合の重要な出力です ( 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

ご覧のとおり、Fetch-And-Add 命令を使用していませんa++

于 2012-11-15T16:19:05.460 に答える
0

最初の質問について: 読み取りと書き込みはアトミックですが、読み取り/書き込み操作はそうではありません。プリミティブに関する特定の参照を見つけることができませんでしたが、JLS #17.7は参照に関して同様のことを言っています:

リファレンスの書き込みと読み取りは、それらが 32 ビットまたは 64 ビットの値として実装されているかどうかに関係なく、常にアトミックです。

したがって、あなたの場合、iload と istore の両方がアトミックですが、(iload、istore) 操作全体はそうではありません。

i++操作を同期する必要がまったくない[と考える]のは間違っていますか?

2番目の質問に関して、以下のコードは私のx86マシンで982を出力します(1,000ではありません)。これは、一部が翻訳で失われたことを示しています==>フェッチと追加をサポートするプロセッサアーキテクチャでも、操作++を適切に同期する必要があります++命令。

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 に答える