序文
「as-if-serial」原則は、Java メモリ モデルにおける重要な概念です。このルールは、どのように並べ替えても(コンパイル中の並べ替え、命令レベルの並列処理の並べ替え、メモリ システムの並べ替えなど)、(シングル スレッドの)プログラムの実行結果を変更することはできません。コンパイラ、ランタイム、プロセッサはすべて、as-if-serial セマンティクスに従う必要があります。
より良いパフォーマンスを得るために、コンパイラーとプロセッサーは命令の順序を変更することがよくありますが、データの依存関係に準拠する必要があります。つまり、シングルスレッド プログラムの実行結果を変更せずに命令を順序変更する必要があります。たとえば、次のコードの場合:
int a = 1; //语句1
int b = 2; //语句2
int c = a + b; //语句3
ステートメント 1 とステートメント 2 にはデータの依存関係がないため、順序を変更できます。ただし、ステートメント 3 はステートメント 1 とステートメント 2 に依存しているため、ステートメント 1 またはステートメント 2 の前に並べ替えることはできません。
ただし、この原則はシングル スレッドにのみ適用されるため、マルチスレッドの場合は、スレッド間の操作の順序性と可視性を確保するために、事前発生の原則に従う必要があります。
詳しい説明
as-if-serial 原則は、並行プログラミングに関係なく、Java プログラムの実行結果がシリアル化された環境でのプログラムの実行結果と一致する必要があることを意味します。簡単に言うと、実行中にプログラムがどのように並べ替えられても (コンパイラの最適化、プロセッサの最適化など)、最終的な実行結果がシリアル実行の結果と一致している限り、そのような並べ替えは許可されているとみなされます。
たとえば、次のコードを考えてみましょう。
int a = 1;
int b = 2;
int c = a + b;
int b = 2;
as-if-serial 原則によれば、ステートメントは実行中に後の実行に移動される可能性がありますが、int c = a + b;
最終的な実行結果 (c の値) は依然としてシリアル実行の結果と一致します。
ただし、同時実行環境では、as-if-serial 原則により問題が発生する可能性があります。例えば:
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
在并发环境下,如果有两个线程同时执行 increment() 方法,由于 as-if-serial 原则,编译器或处理器可能将 count++ 重排序为两个操作:先读取 count 的值,然后再写回 count+1 的值。在两个线程并发执行的情况下,可能第一个线程读取了 count 的值,然后第二个线程也读取了 count 的值,然后两个线程都将 count+1 的值写回,导致 count 的值只增加了 1,而不是预期的 2。
下面是一个更具体的例子来说明 as-if-serial 原则可能导致的问题:
```java
public class Example {
private int a = 0;
private int flag = 0;
public void writer() {
a = 1; //1
flag = 1; //2
}
public void reader() {
if (flag == 1) //3
{
int i = a; //4
}
}
}
この例では、2 つのスレッドがあり、1 つのスレッドが Writer() メソッドを実行し、もう 1 つのスレッドが Reader() メソッドを実行すると仮定します。as-if-serial 原則に従って、コンパイラまたはプロセッサは、writer() メソッド内の 2 行のコードの順序を入れ替えることがあります。つまり、最初に flag = 1 を実行し、次に a = 1 を実行します。このような並べ替えが発生した場合、reader() メソッドは a が割り当てられる前に flag を 1 として読み取り、その後 a を予期される 1 ではなく 0 として読み取る可能性があります。
マルチスレッド下での問題を解決する
Java では、volatile キーワードを使用してこの問題を解決します。
変数が volatile に変更されると、次の 2 つの特性があります。
-
可視性: スレッドが揮発性変数の値を変更すると、新しい値は他のスレッドに即座に認識されます。
-
命令の並べ替えの最適化を無効にする: 通常の変数は 1 のみを満たしますが、揮発性変更された変数は命令の並べ替えの最適化が禁止されているため、2 を満たすことができます。
これら 2 つのプロパティにより、揮発性変数は同時プログラミングで非常に便利になります。
たとえば、上記の例では、フラグ変数が volatile として宣言されている場合、2 つのスレッドが認識するフラグは常に最新になります。書き込みスレッドがフラグの値を変更すると、読み取りスレッドは直ちにそれを認識します。視認性の問題を解決します。同時に、揮発性変数への書き込み操作は直ちにメイン メモリにフラッシュされるため、書き込み操作後の読み取り操作ではこの新しい値が参照されます。
volatile キーワードには、命令の並べ替えを禁止する追加機能があります。コンパイラーが最適化を実行するとき、コードの実行順序が変更される場合があります。フラグ変数が volatile キーワードで変更されると、コンパイラは変数の前後のコードの順序を変更しません。これにより、順序が保証され、順序変更の問題が解決されます。
事前に発生する原則
これは、データの競合があるかどうか、およびスレッド間の変更操作が他のスレッドから見えるかどうかを判断するために使用される原則です。
事前発生の原則を導入する主な理由は 2 つあります。
-
可視性の問題を解決する: 並行プログラミングでは、スレッドの切り替え、コンパイラの最適化、その他の理由により、あるスレッドによる共有変数の変更が他のスレッドにすぐに表示されない可能性があり、これが可視性の問題につながります。Happens-before 原則により、どの操作が他のスレッドから見えるかを明確に知ることができます。
-
順序付けの問題を解決する: 並行プログラミングでは、命令の並べ替えにより、コードの実行順序が作成した順序と異なる場合があります。Happens-before 原理により、操作の順序を明確に知ることができます。
Happens-before 原則には次のルールが含まれます。
-
プログラム順序規則: スレッド内の各操作は、スレッド内の後続の操作よりも前に発生します。
-
モニター ロック ルール: ロックのロック解除は、その後のロックのロックよりも前に行われます。
-
揮発性変数のルール: 揮発性フィールドへの書き込みは、この揮発性フィールドの後続の読み取りよりも前に行われます。
-
推移性: A が B より前に発生し、B が C より前に発生する場合、A は C より前に発生します。
-
start() ルール: スレッド A が操作 ThreadB.start() を実行する (スレッド B を開始する) 場合、スレッド A の ThreadB.start() 操作はスレッド B の操作よりも前に発生します。
-
join() ルール: スレッド A が操作 ThreadB.join() を実行して正常に戻った場合、スレッド B の操作はすべて、スレッド A が ThreadB.join() 操作から正常に戻る前に発生します。
たとえば、2 つのスレッド A と B があるとします。スレッド A は揮発性変数に書き込み、スレッド B はこの変数を読み取ります。次に、スレッド A の書き込み操作がスレッド B の読み取り操作の前に発生し、スレッド B はスレッド A の書き込みを確認できるようになります。
別の例として、あるスレッドが最初にオブジェクトのロックを解除し、次に別のスレッドがオブジェクトをロックしたとします。その後、最初のスレッドのロック解除操作が 2 番目のスレッドのロック操作の前に行われ、2 番目のスレッドは最初のスレッドのロック解除前のすべての操作を確認できます。
要約する
as-if-serial セマンティクス
このプログラムでは、コンパイラやプロセッサによってコードの順序が変更されることがありますが、プログラムの動作から見ると、あたかもソース コードの順序でシリアルに実行されているかのように見える、これが as-if-serial セマンティクスです。
public class AsIfSerial {
private static int x = 0;
private static int y = 0;
public static void main(String[] args) {
x = 1;
y = 2;
int a = x;
int b = y;
System.out.println("a = " + a + ", b = " + b);
}
}
前に起こることの例
このプログラムでは、スレッド A の flag = true 操作がスレッド B の if (フラグ) 操作の前に発生します。これは、それらの間に volatile 変数ルールと結合ルールがあるためです。したがって、スレッド B は、スレッド A がフラグを true に設定していることを確認できます。
public class HappensBefore {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
flag = true;
});
Thread threadB = new Thread(() -> {
if (flag) {
System.out.println("ThreadB sees flag = true");
}
});
threadA.start();
threadA.join();
threadB.start();
threadB.join();
}
}
as-if-serial は、コンパイラーとプロセッサーが命令の並べ替えなどを含むプログラムに対してさまざまな最適化を実行できるようにするプログラム最適化原則ですが、最適化されたプログラムは、プログラムのソース コードを順次実行した結果と一致する必要があります。これにより、プログラムの正確性が保証され、プログラムの動作効率が向上します。
happens-before は、マルチスレッド プログラム内の 2 つ以上の操作間に存在する可能性のある部分的な順序関係を記述します。操作 A が操作 B の前に発生した場合、A の結果は B に表示されます。つまり、B は A の効果を見ることができます。前発生関係により、マルチスレッド プログラムの正しい同期が保証されます。