1つ、JMM
1.コンセプト
JMM(Javaメモリモデル)、Javaメモリモデル
スレッド間の通信と同期(同時に実行されるアクティブエンティティ)はすべてJMMによって制御されます
Javaスレッド間の通信の場合、JMMは、共有変数へのスレッドの書き込みが別のスレッドに表示されるタイミングを決定します。
異なるスレッド間の操作の相対的な順序を制御するためにプログラムで使用されるメカニズムであるJavaスレッド間の同期
JMMの主な構造は次のとおりです。
- 共有変数を格納するために使用されるメインメモリ(共有メモリ)(ヒープに格納される:オブジェクトインスタンスと配列、メソッド領域に格納される:静的変数)
- ローカルメモリ(ワーキングメモリ)、実行時に各スレッドが使用するメモリ領域、共有変数のコピーを保持します
2.メモリインタラクティブ操作手順
JMMは、次の8つの操作命令を定義しています。
-
読み取りとロード
readは、メインメモリ内のデータを読み取り、それを作業メモリに送信します
loadは、readの変数値を作業メモリー内の変数のコピーに割り当てます。
-
使用して割り当てる
useは、作業メモリー内の変数を実行エンジンに渡します
割り当ては、実行エンジンを変数に割り当てます
-
ストア和書き込み
ストアは、作業メモリー内のデータをメインメモリーに転送します
writeは、ストアからメインメモリに転送された変数を格納します
-
ロック和ロック解除
ロックは、メインメモリ内の変数をスレッド排他状態として識別します
ロック解除メインメモリ内のロックされた変数のロックを解除します
read load
主内存 ----> 工作内存 ----> 共享变量的副本
use assign
工作内存 ----> 执行引擎 ----> 共享变量的副本
write store
共享变量的副本 ----> 工作内存 ----> 主内存
lock/unlock
主内存 ----> 共享变量
3.メモリの可視性
[1]問題は
private static int i = 0;
public static void main(String[] args) {
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
System.out.println("数值被改变为:" + i);
}).start();
while (0 == i) {
}
System.out.println("程序执行完毕");
}
ご覧のとおり、新しく作成されたスレッドi
がメンバー変数を変更しても、プログラムはまだ実行中であり、終了していません。
【2】原因と解決策
JMMモデルによると、メインメモリは1つあり、各スレッドには独自の作業メモリがあります。スレッドがデータを操作するとき、最初にデータを自身の作業メモリーにコピーし、次に変更後にメインメモリーに書き戻します。
デフォルトで宣言された変数と作業メモリー内の変更された操作は、他のスレッドには認識されません
この操作を他のスレッドに表示させたい場合は、volatile修飾子を使用して変数を変更できます。
つまり、volatileはメモリの可視性を保証します
4.アトミシティ
[1]問題は
private static volatile int num = 0;
public static void main(String[] args) {
int loop = 10;
int count = 100;
for (int i = 0; i < loop; i++) {
new Thread(() -> {
int number = addAndGet(count);
System.out.println(Thread.currentThread().getName() + "获取数值:" + number);
}, "线程A").start();
}
for (int j = 0; j < loop; j++) {
new Thread(() -> {
int number = addAndGet(count);
System.out.println(Thread.currentThread().getName() + "获取数值:" + number);
}, "线程B").start();
}
}
private static int addAndGet(int count) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < count; i++) {
num++; // 复合操作
}
return num;
}
増加するたびnum
に、得られる結果は必ずしもcount
の倍数ではないことがわかります。それがされていてもvolatile
変更され、それは無用です
【2】原因と解決策
複数のスレッドが同じメソッドを実行する場合、制限がない場合、各スレッドは実行する権利を取得する機会があります。これにより、複数のスレッドによって同時に実行されるメソッドのデータが変更されます。
したがって、同時に1つのスレッド操作メソッドのみが存在するように制限を追加する必要があります。
この制限はロックすることです
これを実現するには、2つの方法があります。JDK5に表示される同期またはjava.util.concurrent.locks.Lockを使用する
つまり、同期は原子性を保証しますが、揮発性は原子性を保証しません
5.命令の並べ替え
[1]問題は
/* 处理器A执行 */
a = 1;
x = b;
/* 处理器B执行 */
b = 2;
y = a;
/*
* 执行结果
* 初始状态:a = b = 0
* 处理器允许执行后得到的结果:x = y = 0
*/
プロセッサAとプロセッサBがプログラムのシーケンスでメモリアクセスを並行して実行する場合、最終的に考えられるx = y = 0
結果
【2】原因と解決策
なぜなら、代入演算子のaとbを実行すると、内部の書き込みバッファゾーンにデータを書き込むだけで、それらのメインメモリにフラッシュされる可能性があるからです。書き込みバッファは、独自のプロセッサにのみ見えます。したがって、メインメモリ内のデータを読み取るときに、ダーティデータを読み取ることができます
この現象は命令の並べ替えです。プロセッサがメモリ操作を実行する順序は、実際のメモリ操作の順序と一致しない場合があります。
最新のプロセッサはすべて書き込みバッファを使用しているため、すべて書き込み/読み取り操作の並べ替えが可能です
パフォーマンスを向上させるために、コンパイラとプロセッサの両方が命令シーケンスを並べ替えます
源代码 ---> 1:[编译器] ---> 2:[指令级] ---> 3:[内存系统] ---> 最终执行的指令序列
优化重排序 并行重排序 重排序
volatileを使用して変数を変更すると、命令の並べ替えを禁止できます
同期を使用して同期するメソッドまたはコードブロックも、命令の並べ替えを禁止する可能性があります
【3】発生前
JDK5以降、Javaは新しいJSR-133メモリモデルの使用を開始しました。
Happen-beforeは、操作間のメモリの可視性を示すために使用されます
JMMでは、ある操作の実行結果が別の操作に表示される場合、2つの操作の間に発生前の関係があります。これらの2つの操作は、同じスレッドまたは異なるスレッドで行うことができます
注:ここでは、ある操作の実行結果が別の操作に表示されます。前の操作を次の操作の前に実行する必要があるわけではありません。
発生前を理解することがJMMを理解するための鍵です
【4】as-if-serial
どのように並べ替えても、(シングルスレッド)プログラムの実行結果は変更できません。
例えば:
int a = 5; // A
int b = 10; // B
int c = a + b; // C
命令シーケンスをどのように並べ替えても、Cは最後に実行する必要があります。そうしないと、プログラムの実行結果が変更されます。AとBの間にはデータの依存関係がないため、コンパイラとプロセッサはAとBの間で実際の実行順序を並べ替えることができます。
したがって、シングルスレッドの場合、命令の並べ替えの干渉を心配する必要はなく、メモリの可視性の影響を考慮する必要もありません。
[5]揮発性のメモリセマンティクスの読み取りと書き込み
揮発性変数を書き込む場合、JMMは、スレッドに対応する作業メモリー内の共有変数のコピーの値をメインメモリーにフラッシュします。
揮発性変数を読み取るとき、JMMは、スレッドに対応する作業メモリー内の共有変数のコピーを無効としてマークし、メインメモリーから共有変数の値を取得します。
[6]ロックの取得と解放のメモリセマンティクス
スレッドがロックを取得すると、JMMは、スレッドに対応する作業メモリー内の共有変数のコピーを無効としてマークするため、モニターによって保護されるクリティカルセクションコードは、メインメモリから共有変数の値を取得する必要があります。
スレッドがロックを解除すると、JMMは、スレッドに対応する作業メモリー内の共有変数のコピーの値をメインメモリーにフラッシュします。
揮発性書き込みのメモリセマンティクスとスレッド解放ロックのメモリセマンティクスは同じであり、揮発性読み取りのメモリセマンティクスとスレッド取得ロックのメモリセマンティクスは同じであることがわかります。
6、決勝
Finalは、初期化のセキュリティ保証を提供します。オブジェクトが正しく構築されている限り(構築されたオブジェクトへの参照がコンストラクターで「エスケープ」されていない限り)、同期を使用して、スレッドがこれを変更したことを確認できるようにする必要はありません。finalコンストラクターによって初期化された変数の値(finalによって変更されたオブジェクトへの参照)
参照エスケープ:オブジェクトの参照は、コンストラクターが戻る前に他のスレッドによって保持されます
interface EventSource {
void registerListener(EventListener eventListener);
}
class Event {
}
interface EventListener {
void doEvent(Event e);
}
class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
@Override
public void doEvent(Event e) {
doSomething(e);
}
});
}
public void doSomething(Event e) {
}
}
ThisEscapeがEventListenerをリリースすると、ThisEscapeをカプセル化したインスタンスも無条件にリリースされました。内部クラスのインスタンスにはパッケージインスタンスへの暗黙の参照が含まれているため(this)
つまり、安全性の順序は次のとおりです。
- コンストラクターが実行されます
- 作成されたオブジェクト
- 最終的に変更されたオブジェクト参照をオブジェクトのインスタンスにポイントします
7.まとめ
可視性 | アトミシティ | 命令の並べ替えを無効にする | |
---|---|---|---|
揮発性 | √ | √ | |
同期 | √ | √ | |
ロック | √ | ||
最後の | 初期化のみ |