本章のまとめ
1.シンクロの利点と欠点?
2.同期使用はどのような点に注意する必要がありますか?
3.デッドロックとは何ですか?デッドロックを引き起こす条件とは?
上記の質問を各自で確認し、それでも疑問が残る場合は、ください。
知識ベース
この章を修了すると、volatile キーワードをより深く理解するとともに、JMM のスレッドセーフティの処理を通じてスレッドセーフティの 3 つの特性を説明し、volatile と synchronized を比較して両者の違いとシナリオの応用を説明することで、スレッドセーフティについてより深く理解できるようになります。
Volatile
揮発性のキーワードでは、いくつかの予備知識を再確認する必要がありますが、この部分を再確認することで、生徒たちは揮発性のキーワードをよりよく理解できるようになると思います。
並行プログラミングの3つの特徴
並行プログラミングの3つの性質とは、原子性、可視性、秩序性です。このサブセクションでは、この3つの性質とJMMがどのようにそれらを保護しているかを説明することで、この知識の一部を復習します。
原子性
それは単に、一度にすべてが行われるか、一度にすべてが行われないかのいずれかである1つまたは複数の操作を意味します。具体的にはどういうことでしょうか?いくつかの例を通して見てみましょう。
x = 10
あるスレッドが代入操作をすると、まず現在のスレッドの作業メモリにあるコピーxを10に代入し、次にメインメモリに同期してメインメモリにあるxを同時に10に代入します。
実はこれは誤解で、実はメインメモリxに対してすでに2つの代入演算が行われているのです。こっちの気になるラティテュードは1つの代入演算なので、この2つの1つの代入演算がすべて行われたことになります。つまり、代入操作はアトミックなのです。
x = y
この操作文も代入操作のように見えますが、そうではありません。このいわゆる代入は分解する必要があります。
1.メインメモリまたはワーキングメモリからyの値を読み込みます。
2.yの値をxに代入し、xの値をメインメモリに書き込みます。
この2つのステップは共生ではなく、すべてが一度に実行されるわけでも、すべてが一度に実行されないわけでもありません。最初のステップはyの値を読み取ることであり、2番目のステップはxに値を代入することです。この2つのステップは互いに完全に独立していてもよいので、この操作はアトミック操作とは言いません。
x++
これは x = y よりも比較的理解しやすくなっています。
- メインメモリーまたはワーキングメモリーからxの値を読み込みます。
- x x xの自己インクリメント+1
- セルフインクリメント後のxの値を再びxに代入し、メインメモリ内のxの値を変更
もちろん、この3つのステップは共生ではないので、原子操作でもありません。
単純な代入操作に加えて、他の2つの操作は原子操作ではないことがわかります。どの操作がアトミック操作なのか、少しまとめてみました。
- 変数間の代入はアトミックではありません。
- 複数のアトミック演算を組み合わせてもアトミックになるとは限りません。
- 、synchronizedキーワードがコードのアトミック性を保証していることをご存じでしょう。
可視性
可視性とは、マルチスレッド並行プログラミングにおいて、スレッドがメインメモリ内の共有変数の値を変更すると、他のスレッドはすぐにそれを感知し、スレッドのローカル作業メモリ内のコピーの値を追うのではなく、メインメモリから新しい共有変数の値を再取得できるという事実を指します。
研究の前の章では、我々は、同期が並行プログラミングにおける可視性の原則を保証することができます知っている、彼の原則は、監視ロックの排他性を使用して、最大1つだけのスレッドが監視ロックを取得することができ、現在の監視ロックが解放された後にのみ、他のスレッドが再びコードの同期パッケージに入ることができることを保証することです。しかし、その欠点は、スレッドの実行効率が低下することです。
順序付け
順序性とは、コードが実行される順序のことです。JVMはプログラムの動作効率を向上させるために、命令の並び替え機能を提供しているためです。つまり、コードはコーディングした順番に実行されるとは限らず、命令間のデータ依存関係を厳密に守らなければなりませんが、業務間の依存関係は保証されないため、マルチスレッド開発の過程で、順序性に関連する様々な問題が発生します。
簡単な例で見てみましょう。
public class InstructionReorder {
public static class PrintOut {
public void getStr() {
System.out.println(Thread.currentThread().getName() + "ここでは、シングルトンパターンのスレッドセーフについて詳しく説明するつもりはない)。;
}
}
private static boolean initStatus = false;
static PrintOut printOut = null;
public static void main(String[] args) {
new Thread(() -> {
//現在のsingleExampleがすでにインスタンス化されているかどうかを判断する。
if (!initStatus) {
//命令の並べ替えをシミュレートする
initStatus = true;
printOut = new PrintOut();
printOut.getStr();
// //命令の並べ替えを禁止する
// initStatus = true;
} else {
printOut.getStr();
}
}, "1スレッド").start();
new Thread(() -> {
//現在のsingleExampleがすでにインスタンス化されているかどうかを判断する。
if (!initStatus) {
printOut = new PrintOut();
printOut.getStr();
initStatus = true;
} else {
printOut.getStr();
}
}, "スレッド2").start();
}
}
private static boolean initStatus = false;static PrintOut printOut = null;ここでは、2つのスレッドが開始され、共有変数が初期化されたかどうかを判断するために初期化状態を使用し、スレッド1での命令並べ替えシナリオをシミュレートします。
//現在のsingleExampleがすでにインスタンス化されているかどうかを判断する。
if (!initStatus) {
//命令の並べ替えをシミュレートする
initStatus = true;
printOut = new PrintOut();
printOut.getStr();
// //命令の並べ替えを禁止する
// initStatus = true;
} else {
printOut.getStr();
}
出力:
最初のスレッドは、聖歌に戻った
Exception in thread "第二スレッド" javaの.lang.NullPointerException
at src.com.lyf.page5.InstructionReorder.lambda$main$1(InstructionReorder.java:44)
at java.lang.Thread.run(Thread.java:748)
ご覧のように、最初のスレッドが開始された後、initStatusのステータス値がtrueに変更され、2番目のスレッドが開始された後、現在のオブジェクトは初期化完了状態であると判断され、printOut.getStr();が直接呼び出され、ヌル・ポインタ例外がスローされます。
これまでのところ、問題の順序によって引き起こされるJVM命令の並べ替えが説明されています〜、学生は何が起こっているかの順序を一般的に理解する必要があります。
好奇心旺盛な学生たちは、もしそうなら、並び替えは落とし穴ではないのに、なぜこの機能を搭載するのですか?
通常、コマンドの並び替えはデータ間の厳密な依存関係に従い、happen-beforeの原則に従います。では、happen-beforeの原則とは一体何なのか、話を進めましょう。
happen-before
- プログラム順序の原則:スレッドでは、それが書かれた順序に従ってコードは、JVMは、プログラムコードの命令を並べ替えることがありますが、彼はスレッドの結果と同じの逐次実行の結果を保証します。ここでの主なポイントの1つは、スレッドですので、彼は実行の順序に並行プログラミングプログラムを保証するものではありません実行の望ましい順序でなければならない、つまり、問題の順序の上記の例です。
- ロックの原則:マルチスレッド環境でもシングルスレッド環境でも、同じロックが一度ロック状態になると、他のスレッドが再びロックをロックする必要がある場合、ロックのロック解除状態を呼び出す必要があります。例として、スレッド A がモニター ロック MUTEX をロックし、スレッド B が MUTEX を取得する必要がある場合、モニター ロック MUTEX のロック解除操作を呼び出す必要があります。
- 揮発性の原則:揮発性の変数に対する書き込み操作は、その変数に対する読み取り操作の前に発生します。簡単に言うと、2つのスレッドが共有変数xを読み書きし、Aがxを読み、Bがxを書き込む場合、Bの書き込み操作はAの読み取り操作の前に行われなければなりません。
- パスの原則:A,B,Cの3つのスレッド操作、AはBの前に、BはCの前に、そしてAはCの前に。
- スレッド開始の原則:スレッド実行ロジックの操作は、スレッドが実際に開始された後にのみ実行される必要があります。つまり、startを呼び出してCPUの実行が正しく行われたとき、スレッドは本当に実行されます。
- スレッド割り込みの原則:スレッド割り込み()メソッドの実行は、割り込み信号のキャプチャよりも優先されなければなりません。
- スレッド終了の原則:スレッド内のすべての操作は、スレッドの終了検出に優先的に発生する必要があります。一般的に、スレッドのタスク実行、ロジックユニットの実行は、スレッドの死で発生する必要があります。
- オブジェクトの終了原則:オブジェクトの初期化が完了するのは、オブジェクトがリサイクルされるときでなければなりません。
第二に、揮発性分析
揮発性の2つの重要な役割
- 共有変数の可視性が保証されます。
- 順序性を保証するため、命令の並べ替えは禁止されています。
volatile保証された可視性
この機能を説明するために例を示します。
コードを渡してください:
public class VolatileTest {
final static int MAX = 5;
// static volatile int init_value = 0;
static int init_value = 0;
public static void main(String[] args) {
new Thread(() ->
{
int localValue = init_value;
while (localValue < MAX) {
if (init_value != localValue) {
System.out.println("value read to "+init_value);
localValue = init_value;
}
}
}, "Reader").start();
//読み取りスレッドが最初に開始されることを確認する
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() ->
{
int localValue = init_value;
while (localValue < MAX) {
System.out.println("value update to "+(++localValue));
init_value = localValue;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Updater").start();
}
}
まず、volatile キーワード修飾子なしで
出力:
value update to 1
value update to 2
value update to 3
value update to 4
value update to 5
さらにその後、アウトプット:
value update to 1
value read to 1
value update to 2
value read to 2
value update to 3
value read to 3
value update to 4
value read to 4
value update to 5
value read to 5
これに対して、readスレッドが最初に起動する場合、volatileキーワードを追加しないと、readスレッドは毎回自身のワークスペースinit_valueとローカル変数init_valueを読み込みますが、これはreadスレッドがinit_valueの変化を感知できない場合に常に等しくなることがわかります。これは理解できます。なぜなら、毎回読み込まれるのはワークスペース内の値だからです。しかし、メインメモリ内の値はとっくに変更されているので、volatileキーワードが追加されると、読み取りスレッドはメインメモリ内のinit_valueの更新を感知できるようになります。具体的には、次のような雰囲気になります。
1.読み取りスレッドはメイン・メモリからinit_value=0を読み取り、自身のローカル・ワークスペースに存在します。
2.更新スレッドは、メインメモリからinit_value=0を読み出し、自身のローカルワークスペースに格納します。また、その値に対して累積演算を行い、init_value=1とします。これにより、自身のローカルワークスペースとメインメモリの値が更新され、共有変数を保持するすべてのスレッドに、共有変数の値が期限切れになったことが通知されます。
init_value =localValue3.読み込みスレッドは、ローカル・ワークスペースのinit_valueが期限切れで、whileループ中に使用できないことを発見します。
volatile保証された順序
volatileは、JVMとプロセッサがvolatileキーワードによって変更された命令を並び替えることを禁止します。単純に理解すると、あるステートメントがvolatileによって変更されると、このステートメントはバリアとなり、彼の前は常に彼の前となり、彼の後ろは常に彼の後ろとなります。しかし、先行するステートメントと後続するステートメントとの間では、依然として命令の順序が入れ替わります。例として
int a = 0;
int b = 0;
volatile int C = 2;
int d = 1;
int e = 2;
C=2の代入演算では、a=0とb=0が完了しているはずですが、ここでaの第1代入が完了しているか、bの第1代入が完了しているかについては、命令の並び替えがある可能性があります。同様に、dとeの代入演算はC=2の代入演算の後でなければならず、両者の間で命令の並び替えが発生する可能性があります。
volatile原子性は保証されません
まず、アトミック性とは、すべてが一度に完了するか、すべてが一度に完了しないかのどちらかであることを思い出してください。一般に、CPUがスケジューリングのためにスレッドをポーリングすることは信用できません。volatileキーワードがどのように効果を発揮するか、以下のプロセスを注意深く確認し、volatileキーワードを使用する場合に考えられるシナリオを想像してください:
1.同時に2つのスレッドA,Bを作成し、sumを累積し、異なるスレッド間で共有される変数の可視性を確保するためにvolatileキーワードでsumを変更します。
2.Aは、一時的にスレッドBに、Aをあきらめるために、この時点で、CPUの実行権を合計= 1に読み出し、Bは、CPUの実行権は、Aまで、蓄積を完了し、リフレッシュの値に自分のスレッドのワークスペースが、まだメインメモリをリフレッシュしていない、BにCPUの実行権。
3.BはAと同じ演算を行い、Bは値sum=2をメインメモリに書き込み、CPUの実行はAに委ねます。
4.Aはまた、メインメモリに書き込まれた和= 2は、この時点で明らかに2倍の蓄積が、結果は1回のみカウントされることがわかりました。
生徒たちがここで重要なポイントを理解できたかどうかはわかりません。ここでは2つに分けて見ることができるようです。
1.どのような操作であっても、作業メモリとメイン・メモリへの書き込みという2つのステップは別々にスレッドセーフですが、2つのステップを一緒に実行することはスレッドセーフではなく、アトミック操作でもありません。これは、異なるスレッドがこの2つのステップを交互に実行できるためです。
2.揮発性の変更は、変数なので、彼はスレッドセーフであることが保証されていない変数固有の操作は、C + +の操作のための揮発性int C = 0と同様に、原子性を保証することはできません。
要約すると、volatile は単一の変数に対する読み書きの可視性と順序に関係し、この変数によって実行される他のすべてのアクションがアトミックであることを保証しないため、スレッドセーフであることは保証されません。以下は volatile 変数の例です、
volatilesynchronizedとの比較
マルチスレッドプログラミングの3つの特徴
1.視認性
volatile: 他のスレッドの作業メモリにあるデータを、命令ロックを使って強制的に無効にし、メインメモリから取り出すようにします。
同期:排他的、同時にモニターロックを取得できるのは1つのスレッドのみ
2.原子性
揮発性:保証なし
同期:原子性を保証する排他性
3.秩序
volatile: JVM 命令の並べ替えを無効にすることで順序を保証します。
同期:秩序を確保するための排他性
パフォーマンスの比較
synchronizedは一見よさそうに見えますが、直列化プログラミングによってスレッドセーフであるため、パフォーマンスが犠牲になり、スレッドブロッキングにつながる可能性があるという欠点があります。volatileは原子性を保証せず、スレッドセーフの観点からは大きな制約がありますが、ブロッキング状態に陥らないという利点があり、プログラムのパフォーマンスは少し良くなります。
volatileキーワードの使用シナリオ
原子性すら保証できないのに、何のためにあなたが必要なのかと生徒たちは思うかもしれません。
いくつかの制限はありますが、適切にvolatileキーワードを使用すれば、まだ良い効果があります:
1.変数への書き込み操作は、変数の現在値に依存します。
スイッチングや状態制御が揮発性の良いところであることは理解しやすい。
2.揮発性の変更された変数に対する操作は、スレッドセーフである必要があります。
これは2つの意味で理解できます。1つ目は、揮発性変更変数は読み取り/書き込み操作であり、JVMは読み取り/書き込み変数のみが、可視性と順序を提供しながら、スレッドセキュリティを保証します。2つ目は、様々な非アトミック操作の揮発性変更変数は、そのスレッドセキュリティを保護するためにロックを使用しているということです。
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
//volatile 役割はここにある
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
興味のある学生は、上記のコードを見てすることができます、この側は、可視性の原則を提供し、同時に揮発性も、複数のインスタンス変数の初期化順序を確保するために、命令の並べ替えを禁止することができます詳細な説明は、シングルトンモードのスレッドセーフのために、ここでは当分の間展開されません。
この章では、学生が少し消化する必要があるより多くの知識のポイントになります、私はああ尋ねたい次の問題。さて〜この問題は、ここで最後まで学習します!たくさんの賞賛サポートありがとうございます〜〜〜〜。





