揮発性の
ハードウェアの向上に伴い、マシンのコア数がシングルコアからマルチコアに変化し、マシンの稼働率を向上させるために、同時プログラミングの重要性がますます高まっており、仕事や現場での最優先事項となっています。並行プログラミングをよりよく理解して使用するには、独自の Java 並行プログラミング知識システムを構築する必要があります。
この記事では、Java の volatile キーワードに焦点を当て、アトミック性、可視性、順序付け、volatile の役割、実装原理、使用シナリオ、および JMM や擬似共有などの関連問題について、シンプルかつわかりやすい方法で説明します。
揮発性をより適切に説明するために、まずその前提となる知識である秩序性、可視性、原子性について話しましょう。
秩序
秩序とは何ですか?
プログラミングに高級言語と単純な構文を使用する場合、最終的にはその言語を CPU が理解できる命令に変換する必要があります。
作業を行うのは CPU であるため、CPU 使用率を高速化するために、プロセス制御の命令の順序が変更されます。
Java メモリ モデルでは、命令の並べ替えルールが前発生ルールを満たす必要があります。たとえば、スレッドの起動は、スレッドの他の操作より前に発生する必要があります。スレッドを開始する命令をスレッドに対して並べ替えることはできません。実行されたタスク
つまり、Javaメモリモデルでは、指定したシングルスレッドの実行プロセスには命令のリオーダリングは影響しませんが、マルチスレッドの場合、各スレッドの実行プロセスを推定することができません。
より適切な説明については、次のコード部分を参照してください。
static int a, b, x, y;
public static void main(String[] args){
long count = 0;
while (true) {
count++;
a = 0;b = 0;x = 0;y = 0;
Thread thread1 = new Thread(() -> {
a = 1;
x = b;
});
Thread thread2 = new Thread(() -> {
b = 1;
y = a;
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (Exception e) {}
if (x == 0 && y == 0) {
break;
}
}
//count=118960,x=0,y=0
System.out.println("count=" + count + ",x=" + x + ",y=" + y);
}
4つの変数a、b、x、yを0に初期化します。
私たちの考えによれば、実行順序は次のとおりです。
//线程1
a = 1;
x = b;
//线程2
b = 1;
y = a;
ただし、命令の順序が変更された後は、次の 4 つの状況が発生する可能性があります。
//线程1
//1 2 3 4
a = 1; a = 1; x = b; x = b;
x = b; x = b; a = 1; a = 1;
//线程2
//1 2 3 4
b = 1; y = a; b = 1; y = a;
y = a; b = 1; y = a; b = 1;
4 番目の状況が発生すると、x と y の両方が 0 になる可能性があります。
では、どうすれば秩序を確保できるのでしょうか?
volatile を使用して変数を変更し、順序性を確保します
CPU 使用率を向上させるために、命令の順序が変更されますが、順序変更によって保証できるのは、単一スレッドでのプロセスの実行ロジックのみです。
マルチスレッドでは実行順序は予測できず、順序性は保証できません。マルチスレッドで順序性を確保したい場合は、volatile を使用できます。Volatile はメモリ バリアを使用して命令の並べ替えを禁止し、順序性を実現します。
同期実行を保証するために直接ロックすることもできます。
同時に、コンカレント パッケージ内のクラスのメモリ バリアを使用して、Unsafe
並べ替えを禁止することができます。
//线程1
a = 1;
unsafe.fullFence();
x = b;
//线程2
b = 1;
unsafe.fullFence();
y = a;
可視性
可視性とは何ですか?
Java メモリ モデルでは、各スレッドには独自の作業メモリとメイン メモリがあります。データを読み取る場合は、データをメイン メモリから作業メモリにコピーする必要があります。データを変更する場合は、独自の作業メモリ内でのみ変更されます。複数のスレッドがある場合 特定のデータが同時に操作され、その変更がメイン メモリに書き戻されない場合、他のスレッドはデータの変更を検出できません。
たとえば、次のコードでは、他のスレッドによる変数の変更を感知できないため、作成されたスレッドはループを続けます。
//nonVolatileNumber 是未被volatile修饰的
new Thread(() -> {
while (nonVolatileNumber == 0) {
}
}).start();
TimeUnit.SECONDS.sleep(1);
nonVolatileNumber = 100;
では、この変数を表示するにはどうすればよいでしょうか?
この変数に対して volatile 変更を使用して可視性を確保することも、同期してロックすることもできます。ロック後は、メイン メモリ内のデータを再読み取りするのと同じです。
原子性
原子性とは何ですか?
実際には、1 つまたは複数の操作を同時に完了できるかどうかが問題であり、そうでない場合は、一部が成功し一部が失敗するのではなく、すべてが失敗する必要があります。
Java メモリ モデルの読み取りおよびロード (上記) 命令のアトミック性は、仮想マシンによって実装されます。
変数の自動インクリメントを使用するには、実際には、まずメイン メモリから変数を読み取り、次に変更し、最後にメイン メモリに書き戻す必要があります。
では、volatile はアトミック性を保証できるのでしょうか?
2 つのスレッドを使用して、volatile で変更された同じ変数を 1 万回インクリメントします。
private volatile int num = 0;
public static void main(String[] args) throws InterruptedException {
C_VolatileAndAtomic test = new C_VolatileAndAtomic();
Thread t1 = new Thread(() -> {
forAdd(test);
});
Thread t2 = new Thread(() -> {
forAdd(test);
});
t1.start();
t2.start();
t1.join();
t2.join();
//13710
System.out.println(test.num);
}
/**
* 循环自增一万次
*
* @param test
*/
private static void forAdd(C_VolatileAndAtomic test) {
for (int i = 0; i < 10000; i++) {
test.num++;
}
}
残念ながら、結果は 20000 ではありません。これは、volatile で変更された変数がそのアトミック性を保証できないことを示しています。
では、どのような方法で原子性を確保できるのでしょうか?
同期ロック方式は、同期ロック方式のみが同時にロックにアクセスできるため、原子性を確保できます。
アトミック クラスを使用すると、CAS を使用する基本的なメソッドでもアトミック性を確保できます。それについては後続の記事で説明します
揮発性原理
順序付け、可視性、アトミック性を記述してテストした後、 volatile は順序付けと可視性を保証できますが、アトミック性は保証できないことがわかります。
では、不安定な最下層はどのようにして秩序性と可視性を実現するのでしょうか?
JVM は、 volatile で変更された変数に volatile アクセス フラグを追加し、オペレーティング システムのメモリ バリアを使用して、バイトコード命令の実行時に命令の並べ替えを禁止します。
一般的に使用されるユニバーサル メモリ バリアは、storeload、store1、storeload、load2 で、書き込み命令がバリアより下に再配置されることを禁止し、読み取り命令がバリアより上に再配置されることを禁止します。つまり、ストアによって書き戻されたメモリは可視 (認識可能) です。後続のロード読み取りはメモリから読み取られます。
揮発性アセンブリ命令の実装は、実際にはロック プレフィックス命令です。
単一のコアは順序性、可視性、および原子性を保証できるため、ロック プレフィックス命令は単一のコアには影響しません。
ロック プレフィックス命令は、マルチコアでデータを変更するときにデータをメモリに書き戻します。メモリに書き戻すには、同時に 1 つのプロセッサだけが動作するようにする必要があります。これはバスをロックすることで実行できますが、他のプロセッサは動作しませんアクセスできません。
同時実行の粒度を向上させるために、プロセッサはキャッシュ ロック (キャッシュ ラインのみのロック) をサポートし、キャッシュ整合性プロトコルを使用して、同じキャッシュ ライン データが同時に変更できないようにします。
メモリに書き戻した後、スニッフィング技術を使用して、他のプロセッサがデータの変更を検知し、その後使用する前にメモリを再読み取りできるようにします。
誤った共有の問題
各読み取りはキャッシュ ラインに対する操作であるため、複数のスレッドが同じキャッシュ ライン内の 2 つの変数を頻繁に変更すると、そのキャッシュ ラインを使用する他のプロセッサが常にデータを再度読み取る必要が生じますか?
これは実際にはいわゆる擬似共有問題です
たとえば、2 つの変数 i1 と i2 が同じキャッシュ ライン内にあります。プロセッサ 1 は i1 に頻繁に書き込み、プロセッサ 2 は i2 に頻繁に書き込みます。i1 と i2 は両方とも volatile によって変更され、これにより i1 も変更されます。 2 はキャッシュ ラインがダーティであることを感知するため、最新のキャッシュ ラインを取得するためにメモリを再読み取りする必要がありますが、そのようなパフォーマンスのオーバーヘッドはプロセッサ 2 が i2 に書き込む意味を持ちません。
フォールス シェアリングの問題を解決する一般的な方法は、これら 2 つのフィールドが同じキャッシュ ライン上に存在しないように、これら 2 つのフィールドの間に十分なフィールドを追加することですが、これもスペースの無駄につながります。
偽共有の問題を解決するために、JDK は@sun.misc.Contended
フィールドへの入力に役立つアノテーションも提供します。
次のコードは、2 つのスレッドを 10 億回ループさせて自己インクリメントを実行します。フォールス シェアリングの問題が発生した場合は 30 秒以上かかりますが、フォールス シェアリングの問題が発生しない場合は数秒しかかかりません。
@sun.misc.Contended
注釈を使用する場合は、JVM パラメータを運ぶ必要があることに注意してください。-XX:-RestrictContended
@sun.misc.Contended
private volatile int i1 = 0;
@sun.misc.Contended
private volatile int i2 = 0;
public static void main(String[] args) throws InterruptedException {
D_VolatileAndFalseSharding test = new D_VolatileAndFalseSharding();
int count = 1_000_000_000;
Thread t1 = new Thread(() -> {
for (int i = 0; i < count; i++) {
test.i1++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < count; i++) {
test.i2++;
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
//31910 i1:1000000000 i2:1000000000
//使用@sun.misc.Contended解决伪共享问题 需要携带JVM参数 -XX:-RestrictContended
//5961 i1:1000000000 i2:1000000000
System.out.println((System.currentTimeMillis() - start) + " i1:"+ test.i1 + " i2:"+ test.i2);
}
不安定な使用シナリオ
Volatile は、可視性と秩序性を確保するために、メモリバリアを介した命令の並べ替えを禁止します。
可視性の特性に基づいて、 volatile は可視性を保証し、ロックなしの読み取り操作によりオーバーヘッドが非常に小さいため、同時プログラミングでのシナリオの読み取りでの使用に非常に適しています。
例: コンカレント パッケージ内の AQS キューの同期ステータス ステータスは volatile で変更されます。
書き込み操作では、多くの場合、ロック同期の保証が必要です
順序性の特性に基づいて、volatile はロックをダブルチェックするときにオブジェクト作成命令の並べ替えを禁止することができ、それによって他のスレッドがまだ初期化されていないオブジェクトを取得するのを防ぎます。
オブジェクトの作成は 3 つのステップに分けることができます。
//1.分配内存
//2.初始化对象
//3.将对象指向分配的空间
ステップ 2 と 3 は両方ともステップ 1 に依存しているため、ステップ 1 を並べ替えることはできません。また、ステップ 2 と 3 には依存関係がないため、並べ替えを行うと、オブジェクトが最初に割り当てられた領域を指し、それから初期化される可能性があります。
このときの二重検出ロックにおいて、スレッドがたまたま空ではないと判断してこのオブジェクトを使用するが、この時点で初期化されていない場合、取得したオブジェクトがまだ初期化されていないという問題が発生する可能性があります。
したがって、正しい二重チェック ロックには、再順序付けを禁止するために volatile を追加する必要があります。
private static volatile Singleton singleton;
public static Singleton getSingleton(){
if (Objects.isNull(singleton)){
//有可能很多线程阻塞到拿锁,拿完锁再判断一次
synchronized (Singleton.class){
if (Objects.isNull(singleton)){
singleton = new Singleton();
}
}
}
return singleton;
}
要約する
この記事では、volatile キーワードに焦点を当てて、秩序性、可視性、原子性、JMM、volatile 原則、使用シナリオ、疑似共有の問題などについて説明します。
CPU使用率を向上させるために命令の順序を変更しますが、シングルスレッドではポインティング処理には影響しませんが、マルチスレッドでは実行処理が予測できません。
Java メモリ モデルでは、各スレッドには独自の作業メモリがあります。データの読み取りにはメイン メモリから読み取る必要があり、データの変更にはメイン メモリに書き戻す必要があります。並行プログラミングでは、他のスレッドがメモリの動作を感知できない場合、変数が変更されました。使用し続けるとエラーが発生する可能性があります
Volatile は、秩序性と可視性を実現するためにメモリ バリアを介した命令の並べ替えを禁止しますが、アトミック性を満たすことはできません。
volatile の基礎となるアセンブリは、ロック プレフィックス命令を使用して実装されます。マルチコアでデータを変更する場合、データをメモリに書き戻すためにバスがロックされます。バスのロックにはコストがかかるため、キャッシュ ラインは後でロックされ、同時に 1 つのプロセスのみが処理できるようにするためにキャッシュ整合性プロトコルが使用されます。プロセッサは同じキャッシュ ラインを変更し、スニッフィング テクノロジを使用して、キャッシュ ラインを所有する他のプロセッサがそれを認識できるようにします。キャッシュ ラインがダーティであるため、その後再読み取りします。
複数のスレッドで頻繁に書き込まれる変数が同じキャッシュラインにある場合、フォールスシェアリングの問題が発生するため、同じキャッシュラインに存在しないようにフィールドを埋める必要があります。
可視性の特性に基づいて、volatile は同時プログラミングでロックフリー読み取り操作を実装するためによく使用されます。順序性の特性に基づいて、取得されたインスタンスが二重検出ロックで初期化されていないことを保証できます。
最後に(ただでやらないで、助けを求めるために連続3回押してください〜)
この記事は、Java コンカレント プログラミングの知識体系をわかりやすく構築するためのコラム「点から線、線から面へ」に収録されていますので、興味のある方は引き続き注目してください。
この記事のメモとケースはgitee-StudyJavaおよびgithub-StudyJavaに含まれています。興味のある学生は stat~ で引き続き注目してください。
ケースの住所:
Gitee-JavaConcurrentProgramming/src/main/java/A_volatile
Github-JavaConcurrentProgramming/src/main/java/A_volatile
質問がある場合は、コメント欄で話し合ってください。Cai Cai の文章が良いと思う場合は、いいね、フォロー、収集してサポートしてください~
雷軍氏: Xiaomi の新オペレーティング システム ThePaper OS の正式版がパッケージ化されました Gome App の宝くじページのポップアップ ウィンドウが創設者を侮辱 米 政府が NVIDIA H800 GPU の中国への輸出を制限 Xiaomi ThePaper OS インターフェース マスターが Scratch を使用して RISC-V シミュレータを操作し、正常に実行されました Linux カーネル RustDesk リモート デスクトップ 1.2.3 がリリースされ、Wayland サポートが強化されました Logitech USB レシーバーを取り外した後、Linux カーネルがクラッシュしました DHH の「パッケージング ツール」のシャープ レビュー": フロントエンドはまったくビルドする必要がありません (No Build) JetBrains が技術文書を作成するために Writerside を起動 Node.js 21 用ツールが正式リリース