まとめ
前回の記事で、CPUキャッシングは可視性につながり、スレッドの切り替えは原子性につながり、コンパイルの最適化は順序付けの問題につながると述べました。次に、この記事は最初に可視性と秩序の問題を解決し、今日の主人公であるJavaメモリーモデルを導きます(インタビューが同時に行われると頻繁に評価されます)。
Javaメモリモデルとは何ですか?
CPUキャッシングが可視性を引き起こし、コンパイルの最適化が順序付けの問題につながることを理解したので、最も簡単な方法は、CPUキャッシングとコンパイルの最適化を直接無効にすることです。しかし、そうすることで私たちのパフォーマンスは爆発しそうです〜。必要に応じて無効にする必要があります。
Javaメモリモデルの仕様は非常に複雑ですが、プログラマーの観点からは次のように理解できます。Javaメモリモデルは、JVMがキャッシュとコンパイルの最適化をオンデマンドで無効にする方法を提供する方法を指定します。
これには、volatile、synchronized、finalの3つのキーワードと、6つのHappens-Beforeルールが含まれます。
揮発性キーワード
volatileにはCPUキャッシュを無効にするという意味があり、CPUキャッシュを無効にすると、データ変数はメモリから直接読み書きされます。たとえば、変数を宣言するためにvolatileを使用するvolatile boolean v = false
場合、変数v
を操作するときにメモリから読み書きする必要がありますが、Javaバージョン1.5より前では問題が発生する可能性があります。
次のコードで、スレッドAがwrite
メソッドを実行し、スレッドBがreader
メソッドを実行すると、スレッドBが判断this.v == true
条件に入ったと判断したとすると、このときのxは何でしょうか。
public class VolatileExample {
private int x = 0;
private volatile boolean v = false;
public void write() {
this.x = 666;
this.v = true;
}
public void reader() {
if (this.v == true) {
// 这里的x会是多少呢?
}
}
}
バージョン1.5より前では、値は666または0である可能性があります。この変数x
はキャッシング(揮発性)を無効にしないためですが、バージョン1.5以降では、Happens-Beforeルールにより、値は666である必要があります。
Happens-Beforeルールとは
Happens-Beforeルールは、前の操作の結果が後続の操作から見えることを表現することです。初めてこのルールに触れた場合、多少混乱するかもしれませんが、何度か読んで理解を深めてください。
1.手順の順次ルール
このルールは、スレッド内で、プログラムの順序に従って、前の操作Happens-Beforeの後に後続の操作が続くことを意味します(つまり、前の操作の結果が後続のすべての操作で確認できる)。上記のコードのように、プログラムの順番で:this.x = 666
Happens-Before this.v = true
。
2.揮発性変数のルール
このルールは、揮発性変数への書き込み操作、変数の読み取り前に起こる操作を指します。意味は次のとおりです。変数がスレッドAによって書き込まれたと仮定すると、変数はどのスレッドからも見えるようになります。これは、CPUキャッシュが無効になっていることを意味します。この場合、以前のバージョン1.5との違いはありません。次に、ルール3をもう一度見ると、状況は異なります。
3.伝染性
このルールは、A Happens-BeforeがBで、B Happens-BeforeがCの場合を示しています。次にAが発生する前とC これは推移的な規則です。今すぐコードを見てみましょう(見やすいようにコピーしました)
public class VolatileExample {
private int x = 0;
private volatile boolean v = false;
public void write() {
this.x = 666;
this.v = true;
}
public void reader() {
if (this.v == true) {
// 读取变量x
}
}
}
上記のコードでは、推移的なルールHappens-Befote に従って、this.x = 666
Happens-Before this.v = true
、this.v = true
Happens-Before がわかります。その後、変数が読み取られるとき、このときのインデックスは、スレッドAがメソッド、thread Bがメソッドを実行し、この時点で、前述の推移性ルールに従って、変数read はでなければなりません。これは、バージョン1.5の揮発性セマンティクスの拡張です。そして、もし変数があるため、以前のバージョン1.5、変数はので、キャッシュ(揮発性)を無効にしないかもしれませんああ。读取变量x
this.x = 666
读取变量x
this.v = true
读取变量x
666
write
reader
this.v == true
x
666
x
x
0
4.プロセスにおけるロックのルール
このルールは、Happens-Beforeロックのロック解除操作と、それに続くロックのロック操作を指します。スーパーバイザは一般的な同期プリミティブであり、Javaでは、Javaでのスーパーバイザの実現が同期されます。
プロセスのロックは、Javaで暗黙的に実装されます。たとえば、次のコードは、同期コードブロックに入る前に自動的にロックされ、コードブロックの実行後に自動的にロック解除されます。ここでのロックとロック解除はすべて、コンパイラーによって実装されます。
synchronized(this) { // 此处自动加锁
// x是共享变量,初始值 = 0
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
結合チューブをロック規則、仮定x
0の初期値を、スレッドAブロック12を実行する値となり、その後時にコードブロックにロックを取得するために、スレッドA、Bのスレッドアンロック、あなたが見ることができます実行結果のスレッドx = 12
。これはプロセスのロックのルールです
5.スレッドの開始()ルール
このルールはスレッドの起動に関するもので、メインスレッドAが子スレッドBを開始した後、メインスレッドAが子スレッドBを開始する前の操作を指します。
HappensBeforeで説明する:スレッドAは、スレッドBのstartメソッドを呼び出します。参照コードは次のとおりです。
int x = 0;
public void start() {
Thread thread = new Thread(() -> {
System.out.println(this.x);
});
this.x = 666;
// 主线程启动子线程
thread.start();
}
現時点では、子スレッドに出力される変数x
値は666ですが、試すこともできます。
6.スレッド結合()ルール
このルールはスレッド待機に関するものです。このルールは、メインスレッドAがサブスレッドBの完了を待機していることを示します(メインスレッドAはサブスレッドBを呼び出すことによって実装されますjoin()
)サブスレッドBが完了すると、メインスレッドは操作、ここを参照してくださいHappens-Beforeで説明されているシェア変数の操作を参照してください:サブスレッドBのjoin()
メソッドがスレッドAで呼び出されて正常に戻る場合、サブスレッドBのすべての操作はメインスレッドから呼び出されますサブスレッドB join()
メソッドの後続の操作。コードの方がわかりやすいサンプルコードは次のとおりです。
int x = 0;
public void start() {
Thread thread = new Thread(() -> {
this.x = 666;
});
// 主线程启动子线程
thread.start();
// 主线程调用子线程的join方法进行等待
thread.join();
// 此时的共享变量 x == 666
}
無視されたファイナル
バージョン1.5より前は、値を変更できないことを除いて、final
フィールドは通常のフィールドと同じでした。
1.5以降のJavaメモリモデルでfinal
は、型変数の再配置に制約があります。これで、正しいコンストラクターがエスケープされない限り、コンストラクターで初期化されたfinal
フィールドの最新の値が他のスレッドから見えるようになります。コードは次のとおりです。
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x;
int j = f.y;
}
}
スレッドがreader()
メソッドとf != null
時間を実行するとき、この時点でのfinal
フィールド変更はでf.x
なければなりませんが3
、そうでy
はないため保証できませ4
んfinal
。これがバージョン1.5より前の場合f.x
は、保証されません3
。
では、エスケープとは何ですか?コンストラクタを変更しましょう:
public FinalFieldExample() {
x = 3;
y = 4;
// 此处为逸出
f = this;
}
ここでは保証できないf.x == 3
場合でも、x
変数はされてfinal
修正され、なぜですか?コンストラクタで命令の再配置が発生する可能性があるため、実行は次のようになります。
// 此处为逸出
f = this;
x = 3;
y = 4;
そしてこの時にf.x == 0
。したがって、コンストラクタにはエスケープがありません。最後に変更されたフィールドは問題ありません。詳細なケースについては、このドキュメントを参照してください
まとめ
この記事では、記事の最後の部分でのfinal
制約の再配置を理解していませんでした。私がインターネットで情報を検索し続け、記事で提供された情報を読んだ後にのみ、ゆっくりと理解しました。多分あなたの心は柔軟ではありません。
この記事の主要な中核となるのはHappens-Beforeルールであり、これらのルールを理解しても問題ありません。
参照記事:Geek Time:Java Concurrent Programming Practice 02
個人ブログのURL:https://colablog.cn/
私の記事があなたを助けるなら、あなたは私のWeChatパブリックアカウントをフォローして、できるだけ早くあなたと記事を共有することができます