答えとその後の議論は、私の最初の推論を統合しただけであることがわかりました。私は今、証明の方法で何かを持っています:
- 書き込みスレッドが実行を開始する前に、読み取りスレッドが完全に実行される場合を考えてみます。
- この特定の実行によって作成された同期順序に注意してください。
- スレッドを実時間でシフトして、並列に実行されるようにしますが、同じ同期順序を維持します。
Javaメモリモデルは実時間を参照しないため、これを妨げるものはありません。これで、2つのスレッドが読み取りスレッドと並行して実行され、書き込みスレッドによって実行されたアクションがないことを確認できます。QED。
例1:1つの書き込み、1つの読み取りスレッド
この発見を最大限に心に訴え、現実のものにするために、次のプログラムを検討してください。
static volatile int sharedVar;
public static void main(String[] args) throws Exception {
final long startTime = System.currentTimeMillis();
final long[] aTimes = new long[5], bTimes = new long[5];
final Thread
a = new Thread() { public void run() {
for (int i = 0; i < 5; i++) {
sharedVar = 1;
aTimes[i] = System.currentTimeMillis()-startTime;
briefPause();
}
}},
b = new Thread() { public void run() {
for (int i = 0; i < 5; i++) {
bTimes[i] = sharedVar == 0?
System.currentTimeMillis()-startTime : -1;
briefPause();
}
}};
a.start(); b.start();
a.join(); b.join();
System.out.println("Thread A wrote 1 at: " + Arrays.toString(aTimes));
System.out.println("Thread B read 0 at: " + Arrays.toString(bTimes));
}
static void briefPause() {
try { Thread.sleep(3); }
catch (InterruptedException e) {throw new RuntimeException(e);}
}
JLSに関する限り、これは法的な出力です。
Thread A wrote 1 at: [0, 2, 5, 7, 9]
Thread B read 0 at: [0, 2, 5, 7, 9]
による誤動作のレポートには依存しないことに注意してくださいcurrentTimeMillis
。報告された時間は実際のものです。ただし、実装では、書き込みスレッドのすべてのアクションを、読み取りスレッドのすべてのアクションの後にのみ表示することを選択しました。
例2:読み取りと書き込みの両方の2つのスレッド
今@StephenCは主張し、多くの人が彼に同意するでしょう、それは起こります-明示的に言及していなくても、それでも時間の順序を意味します。したがって、これがどの程度であるかを正確に示す2番目のプログラムを紹介します。
public static void main(String[] args) throws Exception {
final long startTime = System.currentTimeMillis();
final long[] aTimes = new long[5], bTimes = new long[5];
final int[] aVals = new int[5], bVals = new int[5];
final Thread
a = new Thread() { public void run() {
for (int i = 0; i < 5; i++) {
aVals[i] = sharedVar++;
aTimes[i] = System.currentTimeMillis()-startTime;
briefPause();
}
}},
b = new Thread() { public void run() {
for (int i = 0; i < 5; i++) {
bVals[i] = sharedVar++;
bTimes[i] = System.currentTimeMillis()-startTime;
briefPause();
}
}};
a.start(); b.start();
a.join(); b.join();
System.out.format("Thread A read %s at %s\n",
Arrays.toString(aVals), Arrays.toString(aTimes));
System.out.format("Thread B read %s at %s\n",
Arrays.toString(bVals), Arrays.toString(bTimes));
}
コードを理解しやすくするために、これは典型的な実際の結果になります。
Thread A read [0, 2, 3, 6, 8] at [1, 4, 8, 11, 14]
Thread B read [1, 2, 4, 5, 7] at [1, 4, 8, 11, 14]
一方、このようなものを見ることは決して期待できませんが、それでもJMMの基準では合法です。
Thread A read [0, 1, 2, 3, 4] at [1, 4, 8, 11, 14]
Thread B read [5, 6, 7, 8, 9] at [1, 4, 8, 11, 14]
JVMは、スレッドBに時間1で何を読み取らせるかを知るために、実際にはスレッドAが時間14で何を書き込むかを予測する必要があります。これの妥当性、さらには実現可能性は非常に疑わしいものです。
これから、JVM実装が取ることができる次の現実的な自由を定義できます。
スレッドによる中断されない一連のリリースアクションの可視性は、スレッドを中断する取得アクションの前まで安全に延期できます。
リリースと取得という用語は、 JLS§17.4.4で定義されています。
このルールの当然の結果として、書き込みのみを行い、読み取りを行わないスレッドのアクションは、発生前の関係に違反することなく無期限に延期できます。
不安定な概念を片付ける
volatile
修飾子は、実際には2つの異なる概念についてです。
- それに対する行動が起こることを尊重するという確固たる保証-注文する前に;
- 書き込みのタイムリーな公開に向けたランタイムの最善の努力のソフトな約束。
ポイント2は、JLSによって指定されていないことに注意してください。これは、一般的な予想によって発生するものです。約束を破る実装は、明らかに準拠しています。時間の経過とともに、超並列アーキテクチャに移行するにつれて、その約束は確かに非常に柔軟であることが証明される可能性があります。したがって、将来的には、保証と約束の組み合わせが不十分であることが判明することを期待しています。要件に応じて、一方が他方なし、一方が他方のフレーバーが異なる、または他の任意の数の組み合わせが必要になります。