1. 概要

Javaのvolatileキーワードは通常、スレッドセーフを保証しますが、常にそうであるとは限りません。

このチュートリアルでは、共有volatile変数が競合状態につながる可能性があるシナリオを見ていきます。

2. volatile 変数とは何ですか?

他の変数とは異なり、 volatile 変数は、メインメモリに書き込まれ、メインメモリから読み取られます。 CPUは揮発性変数の値をキャッシュしません。

volatile変数を宣言する方法を見てみましょう。

static volatile int count = 0;

3. volatile変数のプロパティ

このセクションでは、volatile変数のいくつかの重要な機能を見ていきます。

3.1. 視認性保証

異なるCPUで実行され、共有された非volatile変数にアクセスする2つのスレッドがあるとします。 さらに、2番目のスレッドが同じ変数を読み取っている間に、最初のスレッドが変数に書き込んでいると仮定します。

各スレッドは、パフォーマンス上の理由から、変数の値をメインメモリからそれぞれのCPUキャッシュにコピーします。

volatile変数の場合、JVMは、値がキャッシュからメインメモリにいつ書き戻されるかを保証しません。

最初のスレッドから更新された値がすぐにメインメモリにフラッシュバックされない場合、2番目のスレッドが古い値を読み取ってしまう可能性があります。

次の図は、上記のシナリオを示しています。

ここで、最初のスレッドは変数countの値を5に更新しました。 ただし、更新された値のメインメモリへのフラッシュバックはすぐには行われません。 したがって、2番目のスレッドは古い値を読み取ります。 これにより、マルチスレッド環境で誤った結果が生じる可能性があります。

一方、 countを揮発性として宣言すると、各スレッドは遅延なしにメインメモリ内の最新の更新値を確認します

これは、volatileキーワードの可視性保証と呼ばれます。 これは、上記のデータの不整合の問題を回避するのに役立ちます。

3.2. 発生-保証前

JVMとCPUは、パフォーマンスを向上させるために、独立した命令を並べ替えて並行して実行する場合があります。

たとえば、独立していて同時に実行できる2つの命令を見てみましょう。

a = b + c;
d = d + 1;

ただし、一部の命令は並列実行できません。後者の命令は前の命令の結果に依存するためです

a = b + c;
d = a + e;

さらに、独立した命令の並べ替えも行うことができます。 これにより、マルチスレッドアプリケーションで誤った動作が発生する可能性があります。

2つの異なる変数にアクセスする2つのスレッドがあるとします。

int num = 10;
boolean flag = false;

さらに、最初のスレッドが num の値をインクリメントし、フラグ true に設定し、2番目のスレッドがフラグまで待機していると仮定します。 trueに設定されます。 そして、フラグの値が true に設定されると、2番目のスレッドはnum。の値を読み取ります。

したがって、最初のスレッドは次の順序で命令を実行する必要があります。

num = num + 10;
flag = true;

しかし、CPUが命令を次のように並べ替えるとします。

flag = true;
num = num + 10;

この場合、フラグが true に設定されるとすぐに、2番目のスレッドが実行を開始します。 また、変数 num はまだ更新されていないため、2番目のスレッドはnumの古い値である10を読み取ります。 これは誤った結果につながります。

ただし、フラグ volatile として宣言した場合、上記の命令の並べ替えは行われませんでした。

変数にvolatileキーワードを適用すると、発生前の保証が提供されるため、命令の並べ替えが防止されます。

これにより、 volatile 変数の書き込み前のすべての命令が、その後に再順序付けされないことが保証されます。 同様に、 volatile 変数の読み取り後の命令は、その前に発生するように並べ替えることはできません。

4. volatile キーワードはいつスレッドセーフを提供しますか?

volatile キーワードは、次の2つのマルチスレッドシナリオで役立ちます。

  • 1つのスレッドだけがvolatile変数に書き込み、他のスレッドがその値を読み取る場合。 したがって、読み取りスレッドは変数の最新の値を確認します。
  • 操作がアトミックであるように、複数のスレッドが共有変数に書き込んでいる場合。 これは、書き込まれる新しい値が前の値に依存しないことを意味します。

5. volatile がスレッドセーフを提供しないのはいつですか?

volatile キーワードは、軽量の同期メカニズムです。

synchronized メソッドまたはブロックとは異なり、1つのスレッドがクリティカルセクションで作業している間、他のスレッドを待機させません。 したがって、 volatile キーワードは、非アトミック操作または複合操作が共有変数で実行される場合、スレッドセーフを提供しません。

インクリメントやデクリメントなどの操作は複合操作です。 これらの操作には、内部的に3つのステップが含まれます。変数の値を読み取り、更新してから、更新された値をメモリに書き戻します。

値の読み取りと新しい値のメモリへの書き込みの間の短い時間のギャップにより、競合状態が発生する可能性があります。 同じ変数で動作している他のスレッドは、その時間ギャップの間に古い値を読み取って操作する場合があります。

さらに、複数のスレッドが同じ共有変数に対して非アトミック操作を実行している場合、それらは互いの結果を上書きする可能性があります。

したがって、スレッドが最初に共有変数の値を読み取って次の値を把握する必要があるような状況では、変数をvolatileとして宣言しても機能しません

6. 例

ここで、例を使用して、変数を volatile としてスレッドセーフではないと宣言する場合に、上記のシナリオを理解しようとします。

このために、countという名前の共有volatile変数を宣言し、ゼロに初期化します。 この変数をインクリメントするメソッドも定義します。

static volatile int count = 0;

void increment() {
    count++;
}

次に、2つのスレッドを作成します t1 t2。 これらのスレッドは、上記の増分操作を1000回呼び出します。

Thread t1 = new Thread(new Runnable() {
    @Override
    public void run() {
        for(int index=0; index<1000; index++) {
            increment();
        }
    }
});

Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        for(int index=0; index<1000; index++) {
            increment();
        }
    }
});

t1.start();
t2.start();

t1.join();
t2.join();

上記のプログラムから、 count変数の最終的な値は2000になると予想される場合があります。 ただし、プログラムを実行するたびに、結果は異なります。 「正しい」値(2000)が出力される場合と、出力されない場合があります。

サンプルプログラムを実行したときに得られた2つの異なる出力を見てみましょう。

value of counter variable: 2000 value of counter variable: 1652

上記の予測できない動作は、両方のスレッドが共有カウント変数に対してインクリメント操作を実行しているためです。 前述のように、インクリメント操作はアトミックではありません。 読み取り、更新、そして変数の新しい値をメインメモリに書き込むという3つの操作を実行します。 したがって、t1t2の両方が同時に実行されている場合、これらの操作のインターリーブが発生する可能性が高くなります。

t1t2が同時に実行され、t1count変数に対してインクリメント操作を実行するとします。 ただし、更新された値をメインメモリに書き戻す前に、スレッドt2はメインメモリからcount変数の値を読み取ります。 この場合、t2は古い値を読み取り、同じ値に対してインクリメント操作を実行します。 これにより、カウント変数の誤った値がメインメモリに更新される可能性があります。 したがって、結果は予想とは異なります–2000。

7. 結論

この記事では、共有変数をvolatileとして宣言することが常にスレッドセーフであるとは限らないことを確認しました。

スレッドセーフを提供し、非アトミック操作の競合状態を回避するには、同期メソッドまたはブロックまたはアトミック変数の両方を使用することが実行可能なソリューションであることを学びました。

いつものように、上記の例の完全なソースコードは、GitHubから入手できます。