この章では、複数のスレッド間で共有される変数の[可視性]の問題と、複数の命令の実行の[順序性]の問題についてさらに詳しく説明します。
5.1Javaメモリモデル
JMMはJavaメモリモデルであり、メインメモリと作業メモリの抽象的な概念を定義します。最下層はCPUレジスタ、キャッシュ、ハードウェアメモリ、CPU命令の最適化などに対応します。
JMMは以下の側面に反映されています
- Atomicity-命令がスレッドコンテキストの切り替えの影響を受けないことを保証します
- 可視性-命令がCPUキャッシュの影響を受けないことを確認します
- 秩序-CPU命令の並列最適化によって命令が影響を受けないことを保証します
5.2可視性
切っても切れないループ
最初に現象を見てみましょう。メインスレッドによる実行変数の変更はtスレッドには表示されないため、tスレッドは停止できません。
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
sleep(1);
run = false; // 线程t不会如预想的停下来
}
どうして?分析:
1。初期状態では、tスレッドはメインメモリからワーキングメモリへの実行値の読み取りを開始しました。
2. tスレッドはメインメモリからrunの値を頻繁に読み取るため、JITコンパイラはrunの値を自身の作業メモリのキャッシュにキャッシュし、メインメモリのrunへのアクセスを減らして効率を向上させます。
3. 1秒後、メインスレッドはrunの値を変更してメインメモリに同期し、tはこの変数の値を自身の作業メモリのキャッシュから読み取ります。結果は常に古い値になります。
解決
Volatile(volatileキーワード)
を使用して、メンバー変数と静的メンバー変数を変更できます。これにより、スレッドが自身のワークキャッシュから変数の値を検索できなくなり、メインメモリでその値を取得する必要があります。揮発性変数はメインメモリを直接操作します
可視性とアトミシティ
前の例に実際の可視性が反映され、複数のスレッド間で保証され、スレッドが揮発性変数を別のスレッドに変更する可能性があります
原子性を保証しない、スレッドのみに書き込む、複数の読み取りスレッド状況:上記例は、バイトコードから次のように理解されます。
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
getstatic run // 线程 t 获取 run true
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false
スレッドセーフを使用したときに前に示した例を比較してください。1つはi ++、もう1つはi--の2つのスレッドです。これらは最新の値を確認することしかできず、命令のインターリーブを解決できません。
// 假设i的初始值为0
getstatic i // 线程2-获取静态变量i的值 线程内i=0
getstatic i // 线程1-获取静态变量i的值 线程内i=0
iconst_1 // 线程1-准备常量1
iadd // 线程1-自增 线程内i=1
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1
iconst_1 // 线程2-准备常量1
isub // 线程2-自减 线程内i=-1
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
同期されたステートメントブロックは、コードブロックのアトミック性を保証するだけでなく、コードブロック内の変数の可視性も保証できることに注意してください。ただし、不利な点は、
同期が重い操作であり、パフォーマンスが比較的低いことです。
前の例の無限ループにSystem.out.println()を追加すると、揮発性修飾子がなくても、スレッドtが見つかります。
実行変数を正しく表示できます。変更されました。理由を考えてみてください。
CPUキャッシュ構造の原理
1.CPUキャッシュ構造
CPUキャッシュを表示
⚡ root@yihang01 ~ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 1
On-line CPU(s) list: 0
Thread(s) per core: 1
Core(s) per socket: 1
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 142
Model name: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
Stepping: 11
CPU MHz: 1992.002
BogoMIPS: 3984.00
Hypervisor vendor: VMware
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 8192K
NUMA node0 CPU(s): 0
速度の比較
CPUキャッシュラインを表示
⚡ root@yihang01 ~ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
64
CPUが取得するメモリアドレスの形式は次のようになります。
[上位ビットグループマーク] [下位ビットインデックス] [オフセット]
2.CPUキャッシュの読み取り
データを読み取るプロセスは次のとおりです。
下位に応じて、キャッシュ内のインデックスを計算して、それが有効かどうかを判断します。
- 0メモリに移動して、新しいデータを読み取り、キャッシュラインを更新します
- 1次に、高レベルのグループマークが一貫しているかどうかを比較します
- 一貫性があり、オフセットに従ってキャッシュされたデータを返します
- 一貫性がない場合は、メモリに移動して新しいデータを読み取り、キャッシュラインを更新します
3.CPUキャッシュの一貫性
MESIプロトコル1.E
、S、およびM状態キャッシュラインはすべてCPU
2の読み取り要求を満たすことができます。E状態キャッシュライン、書き込み要求がある場合、状態はMに変更され、次にメインへの書き込みが行われます。メモリはトリガーされません3。E
状態のキャッシュラインは、キャッシュラインの読み取り操作を監視する必要があります。ある場合は、S状態に変更します。
4. M状態のキャッシュラインは、キャッシュラインの読み取り動作を監視する必要があります。ある場合は、最初に他のキャッシュ(S状態)のキャッシュラインをI状態(つまり
、6のプロセス)に変更します。 、メインメモリに書き込み
ます。S状態5に変更します。S状態キャッシュライン、書き込み要求があります。4のプロセスに従います
。6。S状態キャッシュライン。キャッシュラインの無効化操作を監視する必要があります。 I状態
7に変更します。I状態キャッシュラインに読み取り要求があり、メインメモリから読み取る必要があります
4.メモリバリア
メモリバリア(メモリフェンス)の
可視性
書き込みバリア(フェンス)は、バリアの前の共有変数への変更がメインメモリに同期される
ことを保証し、読み取りバリア(lfence)は、共有変数がバリアの後に読み取られることを保証します。ロードはメインメモリ内の最新データです。
順序性。
書き込みバリアは、命令が並べ替えられたときに、書き込みバリアの前のコードが書き込みバリアの後に配置されないようにします。
読み取りバリアは、命令が並べ替えられたときに、コードが確実に並べ替えられるようにします。読み取りバリアの後は並べ替えられません。コードは読み取りバリアの前にランク付けされます。
5.3秩序
JVMは、正確性に影響を与えることなく、ステートメントの実行順序を調整できます。次のコードを検討してください。
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
iとjのどちらを最初に実行しても、最終結果には影響しないことがわかります。したがって、上記のコードを実際に実行すると、
i = ...;
j = ...;
または
j = ...;
i = ...;のいずれかになります。
この機能は、「命令の再配置」、「命令」と呼ばれます。マルチスレッドでの再配置」は、正確さに影響します。なぜ再配置命令のそのような最適化があるのですか?CPU実行命令の原理から理解しましょう
命令レベルの並列性
1.名詞
より多くの接点をクロックしたクロックサイクル時間の概念、およびCPUのクロックサイクル時間(クロックサイクル時間)は、周波数の逆数に等しく、CPUの意味は、
たとえば、 4Gクロックサイクル時間のCPU周波数は0.25nsであり、比較として、ウォールクロック
サイクル時間は1秒
です。たとえば、追加命令の実行には通常、クロックサイクル時間
CPIが
必要です。一部の命令には、より多くのクロックサイクル時間が必要です。これにより、CPIが発生します。 (平均命令サイクル数命令あたりのサイクル数)
IPC
相互IPC(クロックサイクルあたりの命令数)CPIであり、クロックサイクルあたりの命令数は、ユーザーの複数の
CPU実行時間
+プログラム実行時間のシステムCPU時間で実行できることを示します。前述のように、次の式で表すことができます。
2.命令の並べ替えの最適化
実際、最新のプロセッサは、1クロックサイクルで最長の実行時間でCPU命令を完了するように設計されています。あなたはなぜこれをやっているのですか?命令
はより小さな段階に分割できると考えられます。たとえば、各命令は次のように分割できます。命令フェッチ-命令デコード-命令実行-メモリアクセス-
これらの5つの段階へのデータの書き戻し
用語参照:
- 命令フェッチ(IF)
- 命令デコード(ID)
- 実行(EX)
- メモリアクセス(MEM)
- レジスタライトバック(WB)
プログラムの結果を変更せずに、これらの命令のさまざまな段階を並べ替えて組み合わせることで、命令レベルの並列処理を実現できます。このテクノロジ
は、80年代半ばから90年代半ばまでのコンピューティングアーキテクチャで重要な位置を占めています。
ヒント:
段階的に、分業は効率を改善するための鍵です!
順序の再配置の前提は、たとえば、再配置の順序が結果に影響を与えないことです。
// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2
3.パイプラインをサポートするプロセッサー
最新のCPUは、マルチレベルの命令パイプラインをサポートしています。たとえば、命令フェッチ-命令デコード-実行命令-メモリアクセス-データライトバックの同時実行をサポートするプロセッサは、5段階の命令パイプラインと呼ぶことができます。このとき、CPUは1クロックサイクルで5つの命令の異なるステージを同時に実行できます(実行時間が最も長い複雑な命令に相当)、IPC = 1。本質的に、パイプラインテクノロジはaの実行時間を短縮することはできません。単一の命令ですが、偽装して、コマンドのスループット率が向上しています。
ヒント:
Pentium 4は最大35ステージのパイプラインをサポートしますが、消費電力が大きいために廃止されました
4.スーパースカラープロセッサ
ほとんどのプロセッサには複数の実行ユニットが含まれていますが、すべての計算機能が集中しているわけではなく、整数演算ユニット、浮動小数点演算ユニットなどに分割できるため、複数の命令を並列に取得してデコードすることもできます。 1クロックサイクルで複数の命令を実行します。IPC> 1
揮発性の原理
volatileの基本的な実装原理は、メモリバリア、メモリバリア(メモリフェンス)です。
- 揮発性変数への書き込み命令の後に書き込みバリアが追加されます
- 揮発性変数の読み取り命令の前に読み取りバリアが追加されます
1.可視性を確保する方法
書き込みバリア(sfence)は、バリアがメインメモリに同期される前の共有変数への変更を保証します
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
読み取りバリア(lfence)は、バリアの後で、共有変数の読み取りがメインメモリに最新のデータをロードすることを保証します
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
2.秩序を確保する方法
書き込みバリアは、命令が並べ替えられたときに、書き込みバリアの前のコードが書き込みバリアの後に配置されないようにします。
public void actor2(I_Result r) {
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
読み取りバリアは、命令が並べ替えられたときに、読み取りバリアの後のコードが読み取りバリアの前に配置されないことを保証します\
public void actor1(I_Result r) {
// 读屏障
// ready 是 volatile 读取值带读屏障
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
繰り返しますが、命令インターリーブは解決できません。
- 書き込みバリアは、後続の読み取りが最新の結果を読み取ることができることを保証するだけであり、読み取りがそれより先に進むことを保証するものではありません。
- 秩序の保証は、このスレッドの関連するコードが並べ替えられないことを保証するだけです。
3.ロックの再確認问题
例として、有名なダブルチェックロックシングルトンモードを取り上げます。
public final class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized
synchronized (Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
上記の実現機能は次のとおりです。
- 怠惰なインスタンス化
- getInstance()を初めて使用する場合は、同期を使用してロックし、その後の使用でロックする必要はありません。
- 暗黙的ですが、非常に重要なポイントがあります。最初のifは、同期ブロックの外側にあるINSTANCE変数を使用します。
ただし、マルチスレッド環境では、上記のコードには問題があります。getInstanceメソッドに対応するバイトコードは次のとおりです。
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
その中で
- 17は、オブジェクトを作成し、オブジェクト参照をスタックに配置することを意味します//新しいシングルトン
- 20は、オブジェクト参照のコピーを意味します//参照アドレス
- 21は、オブジェクト参照を使用してコンストラクターを呼び出すことを意味します
- 24は、オブジェクト参照を使用して静的インスタンスに割り当てることを意味します
たぶん、jvmは次のように最適化されます:最初に24を実行し、次に21を実行します。2つのスレッドt1とt2が次の時系列で実行される場合:
キーは0です:コードのgetstatic行はモニターの制御外にあります。前の例の手に負えない人のようです。モニターを調べ
てINSTANCE変数の値を読み取ることができます。
この時点で、t1は次のようになっています。構築メソッドが完全に実行されていません。構築している場合メソッドで実行する初期化操作が多いため、t2が取得するのは、
初期化されていないシングルトンです
。INSTANCEで揮発性の変更を使用すると、命令の再配置を無効にできます。、ただし、JDK5以降では揮発性に注意してください。非常に効果的です。
4.ロック解决を再確認しました
public final class Singleton {
private Singleton() {
}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
揮発性命令の影響はバイトコードには表示されません
// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27: aload_0
28: monitorexit ------------------------> 保证原子性、可见性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
上記のコメントに示すように、メモリバリア(メモリフェンス)は、以下を保証するために、揮発性の変数を読み書き時に追加される
2つの点
の可視性を
- 書き込みバリア(sfence)は、バリアがメインメモリに同期される前のt1での共有変数への変更を保証します
- 読み取りバリア(lfence)は、t2がバリアの後に共有変数を読み取り、メインメモリに最新のデータをロードすることを保証します。
秩序
- 書き込みバリアは、命令が並べ替えられたときに、書き込みバリアの前のコードが書き込みバリアの後に配置されないようにします。
- 読み取りバリアは、命令が並べ替えられたときに、読み取りバリアの後のコードが読み取りバリアの前に配置されないようにします。
下位レベルは、変数を読み書きするときにロック命令を使用して、マルチコアCPU間の可視性と順序を実現することです。
5.発生-前
Happens-beforeは、共有変数の書き込み操作が他のスレッドの読み取り操作に表示されることを規定しています。これは、可視性と順序に関する一連のルールです。次の発生前のルールを除いて、JMMはスレッドがアクセスできることを保証しません。共有変数への書き込み。共有変数の書き込みは、他のスレッドが共有変数を読み取るために表示されます。
- スレッドがmのロックを解除する前の変数への書き込みは、次にmをロックする他のスレッドによる変数の読み取りに表示されます。
static int x;
static Object m = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (m) {
x = 10;
}
}, "t1").start();
new Thread(() -> {
synchronized (m) {
System.out.println(x);
}
}, "t2").start();
}
揮発性変数へのスレッドの書き込みは、他のスレッドによる変数の後続の読み取りに表示されます
volatile static int x;
public static void main(String[] args) {
new Thread(() -> {
x = 10;
}, "t1").start();
new Thread(() -> {
System.out.println(x);
}, "t2").start();
}
スレッド開始前の変数への書き込み、スレッド開始後の変数の読み取りが表示されます
static int x;
x = 10;
new Thread(()->{
System.out.println(x);
},"t2").start();
スレッドの終了前の変数への書き込みは、他のスレッドが変数の終了を認識した後、読み取りに表示されます(たとえば、他のスレッドはt1.isAlive()またはt1.join()を呼び出して、変数が終了するのを待ちます)
static int x;
Thread t1 = new Thread(()->{
x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);
スレッドt1は、t2の前に変数への書き込みを中断し(中断)、t2が中断されたことを知った後(t2.interruptedまたはt2.isInterruptedを介して)、他のスレッドが変数を読み取ることができます。
static int x;
public static void main(String[] args) {
Thread t2 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println(x);
break;
}
}
}, "t2");
t2.start();
new Thread(() -> {
sleep(1);
x = 10;
t2.interrupt();
}, "t1").start();
while (!t2.isInterrupted()) {
Thread.yield();
}
System.out.println(x);
}
変数のデフォルト値(0、false、null)の書き込みは、他のスレッドによる変数の読み取りに
推移的です。xhb-> yおよびyhb-> zの場合、x hb-> z、揮発性注文の再配置には、次の例があります
volatile static int x;
static int y;
new Thread(()->{
y = 10;
x = 20;
},"t1").start();
new Thread(()->{
// x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x);
},"t2").start()
変数は、メンバー変数または静的メンバー変数を
参照します。参照:ページ17
スレッドセーフなシングルトン演習
シングルトンモードには、空腹の人、怠惰な人、静的な内部クラス、列挙型クラスなど、多くの実装メソッドがあります。各実装でシングルトンオブジェクトを取得する(つまり、getInstanceを呼び出す)スレッドセーフを分析して、コメントの問題。
空腹のスタイル:クラスの読み込みにより単一インスタンスのオブジェクトが作成されます
レイジースタイル:クラスの読み込みによって単一インスタンスのオブジェクトが作成されることはありませんが、オブジェクトが初めて使用されるときにのみ作成されます
実装1:
// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
private Singleton() {
}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?
private static final Singleton INSTANCE = new Singleton();
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
実装2:実装2:
// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
INSTANCE;
}
実装3:
public final class Singleton {
private Singleton() { }
private static Singleton INSTANCE = null;
// 分析这里的线程安全, 并说明有什么缺点
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
実装4:DCL
public final class Singleton {
private Singleton() {
}
// 问题1:解释为什么要加 volatile ?
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
if (INSTANCE != null) { // t2
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
実装5:
public final class Singleton {
private Singleton() {
}
// 问题1:属于懒汉式还是饿汉式
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
// 问题2:在创建时是否有并发问题
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
章のまとめ
この章では、
- 可視性-JVMキャッシュの最適化によって引き起こされます
- 秩序-JVM命令の並べ替えの最適化によって引き起こされます
起こる-ルールの
原則の側面の前に
- CPU命令並列
- 揮発性
モデルの側面
- 2段階終了モードでの揮発性の改善
- 同期モードでのボーキング