なぜi ++はアトミックではないのですか?


97

なぜi++Javaではアトミックではないのですか?

Javaを少し深くするために、スレッド内のループが実行される頻度を数えようとしました。

だから私は

private static int total = 0;

メインクラスで。

2つのスレッドがあります。

  • スレッド1:プリント System.out.println("Hello from Thread 1!");
  • スレッド2:プリント System.out.println("Hello from Thread 2!");

そして、スレッド1とスレッド2によって印刷された行を数えますが、スレッド1の行+スレッド2の行は、印刷された行の総数と一致しません。

これが私のコードです:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Test {

    private static int total = 0;
    private static int countT1 = 0;
    private static int countT2 = 0;
    private boolean run = true;

    public Test() {
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        newCachedThreadPool.execute(t1);
        newCachedThreadPool.execute(t2);
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        run = false;
        try {
            Thread.sleep(1000);
        }
        catch (InterruptedException ex) {
            Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println((countT1 + countT2 + " == " + total));
    }

    private Runnable t1 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT1++;
                System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    private Runnable t2 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total++;
                countT2++;
                System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total);
            }
        }
    };

    public static void main(String[] args) {
        new Test();
    }
}

14
やってみませんAtomicIntegerか?
Braj 2014


3
JVMにはiinc整数をインクリメントする操作がありますが、これは、同時実行性が問題にならないローカル変数に対してのみ機能します。フィールドの場合、コンパイラーは読み取り-変更-書き込みコマンドを個別に生成します。
Silly Freak 14

14
なぜそれがアトミックであると予想するのですか?
Hot Licks 14

2
@Silly Freak:iincフィールドに対する命令があったとしても、単一の命令を持つことはアトミック性を保証しません。たとえば、非volatile longおよびdoubleフィールドアクセスは、単一のバイトコード命令によって実行されるという事実に関係なく、アトミックであることが保証されません。
Holger 2014

回答:


125

i++原子性はの大部分の用途には存在しない特別な要件であるため、Javaではおそらく原子的ではありませんi++。この要件にはかなりのオーバーヘッドがあります。インクリメント操作をアトミックにすることには大きなコストがかかります。これには、通常の増分で存在する必要のないソフトウェアレベルとハードウェアレベルの両方での同期が含まれます。

i++非アトミックなインクリメントがを使用して実行されるように、アトミックなインクリメントを実行するように設計および文書化されているはずの引数を作成できますi = i + 1。ただし、これはJavaとCおよびC ++の間の「文化的な互換性」を壊します。同様に、Cライクな言語に精通しているプログラマーが当たり前とする便利な表記を取り除き、限られた状況でのみ適用される特別な意味を与えます。

のような基本的なCまたはC ++コードfor (i = 0; i < LIMIT; i++)はJavaに次のように変換されfor (i = 0; i < LIMIT; i = i + 1)ます。アトミックを使用するのは不適切なためi++です。さらに悪いことに、Cや他のCのような言語からJavaに来るプログラマーはi++とにかく使用してしまい、アトミックな命令を不必要に使用することになります。

マシンインストラクションセットレベルでも、通常、インクリメントタイプの操作はパフォーマンス上の理由からアトミックではありません。x86では、特別な命令「lock prefix」を使用して、inc上記と同じ理由で、命令をアトミックにする必要があります。Ifはinc常にアトミックた非アトミックINCが必要な場合に、それが使用されることはありませんでしょう。プログラマーとコンパイラーは、ロード、1の追加、およびストアを行うコードを生成します。

一部の命令セットアーキテクチャでは、アトミックがないincか、おそらくまったくありませんinc。MIPSでアトミックincを行うには、lland sc:load-linkedとstore-conditional を使用するソフトウェアループを記述する必要があります。load-linkedは単語を読み取り、単語が変更されていない場合はstore-conditionalが新しい値を保存します。そうでない場合は失敗します(検出されて再試行が発生します)。


2
Javaにはポインタがないため、ローカル変数のインクリメントは本質的にスレッドの保存であり、ループを使用すると、問題はほとんどそれほど悪くありません。もちろん、最小の驚きの立場についてのあなたのポイントは。また、そのままでは、i = i + 1の翻訳で++iはなくi++
Silly Freakが

22
質問の最初の言葉は「なぜ」です。今のところ、これが「なぜ」の問題に対処する唯一の答えです。他の答えは本当に質問を言い直してください。だから+1。
Dawood ibnカリーム2014

3
原子性の保証は非volatileフィールドの更新の可視性の問題を解決しないことは注目に値するかもしれません。したがって、volatile1つのスレッドで++演算子が使用された後、すべてのフィールドを暗黙的に処理しない限り、そのような原子性の保証は同時更新の問題を解決しません。それで、問題が解決しないのに、なぜパフォーマンスが無駄になる可能性があるのでしょうか。
Holger 2014

1
@DavidWallaceという意味++ですか?;)
Dan Hlavenka、2014

36

i++ 2つの操作が含まれます。

  1. 現在の値を読み取る i
  2. 値をインクリメントして割り当てます i

2つのスレッドがi++同時に同じ変数を実行すると、両方が同じ現在の値を取得し、iそれをインクリメントしてに設定するためi+1、2つではなく1つのインクリメントが得られます。

例:

int i = 5;
Thread 1 : i++;
           // reads value 5
Thread 2 : i++;
           // reads value 5
Thread 1 : // increments i to 6
Thread 2 : // increments i to 6
           // i == 6 instead of 7

(アトミックでi++ あったとしても、明確に定義された/スレッドセーフな動作ではありません。)
user2864740 '08

15
+1ですが、「1。A、2。BおよびC」は2つの操作ではなく3つの操作のように聞こえます。:)
yshavit 2014

3
格納場所をインクリメントする単一の機械語命令で操作が実装された場合でも、スレッドセーフである保証はありません。マシンはまだ、値を取得し、それをインクリメントし、戻ってそれを保存する必要があり、加えてその保管場所の複数のキャッシュコピーがあるかもしれません。
Hot Licks 14

3
@Aquarelle-2つのプロセッサが同じ保存場所に対して同じ操作を同時に実行し、その場所に「予約」ブロードキャストがない場合、それらはほぼ確実に干渉し、偽の結果を生成します。はい、この操作を "安全"にすることは可能ですが、ハードウェアレベルでも特別な努力が必要です。
ホットリックス2014

6
しかし、問題は「何故」ではなく「なぜ」であったと思います。
セバスチャンマッハ

11

重要なのは、JVMのさまざまな実装が言語の特定の機能を実装しているかどうかにかかわらず、JLS(Java言語仕様)です。JLSは15.14.2節で++後置演算子を定義しています。これは、iaは「値1が変数の値に追加され、合計が変数に戻されて格納される」と述べています。マルチスレッドや原子性について言及または示唆するところはどこにもありません。これらに対して、JLSは揮発性同期化されたものを提供します。さらに、パッケージjava.util.concurrent.atomicがあります(http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.htmlを参照)


5

なぜi ++はJavaでアトミックではないのですか?

インクリメント操作を複数のステートメントに分割しましょう:

スレッド1&2:

  1. 合計のメモリからの値のフェッチ
  2. 値に1を加算します
  3. メモリに書き戻す

同期がない場合は、スレッド1が値3を読み取って4に増やしたが、書き戻していないとしましょう。この時点で、コンテキストの切り替えが発生します。スレッド2は値3を読み取り、値をインクリメントし、コンテキストスイッチが発生します。両方のスレッドが合計値を増やしましたが、それでも4-競合状態になります。


2
これが質問の答えになるはずです。言語は、インクリメントであれユニコーンであれ、あらゆる機能をアトミックとして定義できます。あなたはアトミックでないことの結果を単に例示しています。
セバスチャンマッハ

はい、言語は任意の機能をアトミックとして定義できますが、Javaがインクリメント演算子(OPによって投稿された質問)と見なされる限り、アトミックではなく、私の答えは理由を述べています。
Aniket Thakur 2014

1
(最初のコメントで私の過酷な口調で申し訳ありません)しかし、その理由は「それがアトミックであるならば、競合状態はないから」です。つまり、競合状態が望ましいかのように聞こえます。
セバスチャンマッハ

@phresnelインクリメントアトミックを維持するために導入されるオーバーヘッドは非常に大きく、めったに必要とされないため、操作を安価に保つことができるため、ほとんどの場合、非アトミックが望ましいです。
josefx 2014

4
@josefx:事実については質問していませんが、この回答の理由については質問しています。それは基本的に「競合状態のためにJavaでi ++はアトミックではない」と言っていますこれは、「発生する可能性のあるクラッシュのために車にエアバッグがない」、または「ソーセージはカットする必要があるかもしれません」。したがって、これは答えではないと思います。問題は「i ++は何をするのか」ではなかったまたは「i ++が同期されないことによる影響は何ですか?」
セバスチャンマッハ

5

i++ 3つの操作を含むステートメントです。

  1. 現在の値を読み取る
  2. 新しい値を書き込む
  3. 新しい値を保存

これらの3つの操作は、単一のステップで実行されることを意図してi++おらず、つまり複合操作ではありません 。結果として、複数のスレッドが単一の非複合操作に関係している場合、あらゆる種類のことがうまくいかない可能性があります。

次のシナリオを検討してください。

時間1

Thread A fetches i
Thread B fetches i

時間2

Thread A overwrites i with a new value say -foo-
Thread B overwrites i with a new value say -bar-
Thread B stores -bar- in i

// At this time thread B seems to be more 'active'. Not only does it overwrite 
// its local copy of i but also makes it in time to store -bar- back to 
// 'main' memory (i)

時間3

Thread A attempts to store -foo- in memory effectively overwriting the -bar- 
value (in i) which was just stored by thread B in Time 2.

Thread B has nothing to do here. Its work was done by Time 2. However it was 
all for nothing as -bar- was eventually overwritten by another thread.

そして、そこにあります。競合状態。


それi++がアトミックではない理由です。もしそうなら、これは何もfetch-update-store起こらず、それぞれがアトミックに起こります。それがまさにそのAtomicIntegerためであり、あなたの場合にはおそらくぴったり合うでしょう。

PS

それらすべての問題をカバーする優れた本、そしていくつかはこれです: Javaの並行性の実践


1
うーん。言語は、インクリメントであれユニコーンであれ、あらゆる機能をアトミックとして定義できます。あなたはアトミックでないことの結果を単に例示しています。
セバスチャンマッハ

@phresnelその通りです。しかし、私はまた、単一の操作ではないことを指摘します。これは、複数のそのような操作をアトミック操作に変換するための計算コストがはるかに高価であることを意味しi++ます。
kstratis 2014

1
私はあなたの要点を得ますが、あなたの答えは学習を少し混乱させます。例と、「例の状況のた​​め」という結論が表示されます。imhoこれは不完全な推論です:(
Sebastian Mach

1
@phresnel多分教育学的な答えではないかもしれませんが、私が現在提供できる最高のものです。うまくいけば、それは人々を助け、混乱させないでしょう。しかし、批判をありがとう。今後の投稿では、より正確に説明するつもりです。
kstratis 2014

2

JVMでは、増分には読み取りと書き込みが含まれるため、アトミックではありません。


2

操作i++がアトミックである場合は、その値を読み取る機会がありません。これはまさにi++(を使用する代わりに++i)使用したいことです。

たとえば、次のコードを見てください。

public static void main(final String[] args) {
    int i = 0;
    System.out.println(i++);
}

この場合、出力は次のようになります0 (インクリメントをポストするため(たとえば、最初に読み取り、次に更新するため))。

これは、操作をアトミックにすることができない理由の1つです。これは、値を読み取って(そしてそれに対して何かを行う必要があります)、次に値を更新する必要があるためです。

もう1つの重要な理由は、ロックのために、通常、何かをアトミックに実行すると時間がかかることです。人々がアトミックな操作をしたいというまれなケースで、プリミティブに対するすべての操作に少し長くかかるのはばかげたことでしょう。彼らは追加した理由ですAtomicIntegerし、他の言語へのアトミックなクラス。


2
これは誤解を招くものです。実行と結果を分離する必要があります。そうしないとアトミック操作から値を取得できませんでした。
セバスチャンマッハ

ありませんそれはJavaののAtomicIntegerは、get()、getAndIncrement()、getAndDecrement()、incrementAndGet()、decrementAndGet()などを持っている理由であること、ではありません
ロイ・バン・レイン

1
また、Java言語はi++に拡張するように定義できたはずi.getAndIncrement()です。このような拡張は新しいものではありません。たとえば、C ++のラムダはC ++の匿名クラス定義に拡張されます。
セバスチャンマッハ

アトミックが与えられると、i++自明にアトミックを作成し++iたり、その逆を行うことができます。1つは他の1つに相当します。
David Schwartz、

2

2つのステップがあります。

  1. メモリから私をフェッチ
  2. i + 1をiに設定

したがって、アトミック操作ではありません。thread1がi ++を実行し、thread2がi ++を実行する場合、iの最終値はi + 1になります。


-1

並行性(Threadクラスなど)は、Java v1.0の追加機能です。i++その前にベータ版で追加されました。そのため、(多かれ少なかれ)元の実装では、まだありそうです。

変数を同期させるのはプログラマの責任です。これに関するOracleのチュートリアルを確認してください。

編集:明確にするために、i ++はJavaよりも古くからある明確に定義された手順であり、そのためJavaの設計者はその手順の元の機能を維持することを決定しました。

++演算子はB(1969)で定義されたもので、Javaおよびスレッド化よりも少し前から存在していました。


-1 "パブリッククラススレッド...以降:JDK1.0"ソース:docs.oracle.com/javase/7/docs/api/index.html?java
Silly Freak

バージョンは、それがThreadクラスの前にまだ実装されており、そのため変更されなかったという事実ほど重要ではありませんが、私はあなたの答えを編集しました。
TheBat 14

5
重要なのは、「Threadクラスの前にまだ実装されていた」という主張がソースに裏付けられていないことです。i++アトミックでないことは設計上の決定であり、成長するシステムの見落としではありません。
Silly Freak 14

かわいいです。i ++は、Javaの前に存在していた言語があるという理由だけで、スレッドのかなり前に定義されました。Javaの作成者は、広く受け入れられている手順を再定義する代わりに、これらの他の言語をベースとして使用しました。見落としだとどこで言ったのですか?
TheBat 14

@SillyFreakここでどのように古い++を示し、いくつかの情報源です:en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.