。1つは、Javaメモリモデルです。
1.1コンピュータメモリモデル
命令はCPUによって実行されます。命令は最初にメインメモリから取得され、次に命令が実行されます。このプロセスでは、データの読み取りと書き込みが可能ですが、CPUは読み取りと書き込みよりもはるかに高速に命令を実行します。メインメモリからのデータ。これはCPUリソースの浪費を引き起こします。CPUの効率を向上させるために、キャッシュと呼ばれる高速バッファメモリがCPUとメインメモリの間に追加されます。次に、CPUとキャッシュとメインメモリは次のとおりです
次に、プログラムの実行プロセスは次のとおりです。
- まず、メインメモリからCPUの高速バッファにデータをコピーします
- CPUが計算を実行すると、キャッシュを直接フラッシュしてデータの読み取りと書き込みを行います。
- 操作が終了したら、キャッシュ内のデータをメインメモリに更新します
1.1.1キャッシュの不整合の問題
CPU、キャッシュ、メインメモリで構成される階層構造は、CPUとメインメモリの実行速度の不一致の問題を解決しますが、新しい問題ももたらします。
- シングルコアCPU、シングルスレッド、問題ありません。CPUコアのキャッシュは1つのスレッドによってのみアクセスされます。キャッシュは排他的であり、アクセスの競合などの問題はありません。
- シングルコアCPU、マルチスレッド、CPUがメモリのブロックをキャッシュにロードした後、異なるスレッドが同じ物理アドレスにアクセスすると、異なるスレッドが同じキャッシュの場所にマップされるため、スレッドが切り替えられても、キャッシュは無効にされません。ただし、常に1つのスレッドしか実行されていないため、キャッシュアクセスの競合は発生しません。
- マルチコアCPU、マルチスレッド、各コアには少なくとも1つのレベル1キャッシュがあります(キャッシュは複数にすることができます)。複数のスレッドがプロセス内の共有メモリにアクセスし、これらの複数のスレッドは異なるコアで実行され、各コアは共有メモリバッファを独自のcaeheに保持します。複数のコアを並列化できるため、複数のスレッドがそれぞれのキャッシュに同時に書き込む可能性があり、それぞれのキャッシュ間のデータが異なる可能性があります。
ただし、最近のコンピューターはマルチコアCPUであるため、キャッシュに一貫性がないという問題があります。詳細については、次の説明を参照してください。
i ++操作を実行するスレッドは次の2つです。iの初期値は0です。通常の状態では、2つのスレッドがi ++を実行した結果は2になります。ただし、キャッシュの不整合の問題により、2つのスレッドがあります。次のような状況:
- スレッド1は最初にメインメモリからスレッド1のキャッシュにi = 0を読み取り、次にCPUが操作を完了し、i = 1をメインメモリに書き込み、次にスレッド2がメインメモリ1からキャッシュにi =を読み取ります。スレッド2の場合、CPUは計算を完了し、キャッシュを介してメインメモリにi = 2を書き込みます。これは望ましい結果です。
- スレッド1は最初にメインメモリからスレッド1のキャッシュにi = 0を読み取ります。マルチコアであるため、スレッド2もi = 0を独自のキャッシュに読み取ります。最後に、スレッド1とスレッド2が計算を終了した後、またi = 1をメモリに書き戻すと、最終結果はi = 1になり、キャッシュに一貫性がないという問題があります。
コンピュータメモリモデルのキャッシュの不整合の問題を解決するには、おそらく2つの方法があります。
-
バス
LOCK#
ロックを追加する方法:CPUおよびその他のコンポーネントは通信バスを介して実行されるため、バスに加えてLOCK#
ロックすると、他のメンバーへの他のCPUアクセス(メモリなど)がブロックされ、この変数のメモリを使用できるCPU。バス上でLCOK#
ロック信号が発行されるため、このコードが完全に実行された後にのみ、他のCPUがそのメモリから変数を読み取り、対応する操作を実行できます。 -
キャッシュコヒーレンスプロトコル(キャッシュコヒーレンスプロトコル):キャッシュコヒーレンスプロトコル(キャッシュコヒーレンスプロトコル)を通じて、最も有名なのはIntelのMESIプロトコルであり、MESIプロトコルは、各キャッシュで使用される共有変数のコピーの一貫性を保証します。
MESIのコアアイデアは次のとおりです:CPUがデータを書き込むとき、操作変数が共有変数であることが判明した場合、つまり、変数のコピーが他のCPUに存在する場合、CPUは他のCPUに通知する信号を送信します変数のキャッシュラインを無効にします。したがって、他のCPUがこの変数を読み取る必要があり、変数をキャッシュにキャッシュするキャッシュラインが無効であることがわかった場合、メモリから再読み取りします。
1.1.2プロセッサの並べ替えの問題
マルチスレッドのシナリオでは、ハードウェアの問題もあります。プロセッサ内の算術演算装置を最大限に活用するために、プロセッサは入力コードに対してアウトオブオーダー実行処理を実行する場合があります。これはプロセッサの最適化です。
ここで、プロセッサAとプロセッサBは、共有変数をそれぞれの書き込みバッファ(A1、B1)に同時に書き込み、メモリから別の共有変数(A2、B2)を読み取り、最後にバッファ領域に書き込むことができます。保存されたダーティデータは次のとおりです。メモリ(A3、B3)にフラッシュされます。このシーケンスで実行すると、プログラムはx = y = 0の結果を得ることができます。
メモリ操作が実際に発生する順序から判断すると、プロセッサAがA3を実行して自身の書き込みバッファ領域をリフレッシュするまで、書き込み操作A1が実際に実行されます。プロセッサAによって実行されるメモリ操作の順序はA1→A2ですが、実際のメモリ操作の順序はA2→A1です。このとき、プロセッサAのメモリ動作シーケンスが並べ替えられます(プロセッサBの状況はプロセッサAの状況と同じなので、ここでは繰り返しません)。
1.2Javaメモリモデル
Javaメモリモデル(JMM)は、コンピュータメモリモデルのJavaプログラミング言語仕様です。Javaメモリモデルを使用すると、マルチスレッドシナリオでの原子性、可視性、および順序を保証できます。
JMMは、すべての共有変数(ローカル変数はスレッド間で共有されず、JMMの影響を受けない)がメインメモリに格納されることを規定しています。各スレッドには独自のワークスペースがあります、共有変数に対するスレッドのすべての操作は、ワークスペースで実行する必要があります、メインメモリの共有変数のコピーがワークスペースに保存されます、異なるスレッドが互いのワークスペースに直接アクセスすることはできません、スレッド間でのメッセージパッシングもメインメモリを介して行われます。
JMMでの同期に関する規制:
- スレッドのロックを解除する前に、共有変数の値をメインメモリにフラッシュバックする必要があります
- スレッドがロックする前に、メインメモリの最新の値を自身の作業メモリに読み取る必要があります
- ロックとロック解除は同じロックです
JMMは、原子性、可視性、および順序を保証する必要があります
2、揮発性キーワード
Volatileは、Java仮想マシンによって提供される軽量の同期メカニズムであり、次の3つの特徴があります。
- 可視性を確保する
- 原子性の保証はありません
- 命令の並べ替えを無効にする
2.1可視性を確保する
可視性のない例を見てみましょう。
//资源类
class MyData{
int num = 0;
public void addTo60(){
num = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
//注意这里必须要休眠
try {
TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {
e.printStackTrace();}
myData.addTo60();
System.out.println(Thread.currentThread().getName() + "\t update number value");
},"A").start();
while(myData.num == 0){
}
System.out.println(Thread.currentThread().getName() + "\t主线程结束");
}
}
次の図に示すように、ブロックされています。これは、メインスレッドがwhileループでスタックしていることを意味します。
Aスレッドにコードのブロックがある理由を説明してください。
Aスレッドがブロックされていない場合、num = 60コードの行しか実行されないため、実行速度は非常に速くなります。実行が完了すると、メインスレッドはメインメモリからAスレッドの変更された値を取得します。可視性が見えないこと。
上記のコードから、volatileキーワードを追加しないと、可視性が保証されないことがわかります。その後、volatileキーワードを追加すると、次のような効果が得られます。
class MyData{
//加入volatile关键字
volatile int num = 0;
public void addTo60(){
num = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {
e.printStackTrace();}
myData.addTo60();
System.out.println(Thread.currentThread().getName() + "\t update number value");
},"A").start();
while(myData.num == 0){
}
System.out.println(Thread.currentThread().getName() + "\t主线程结束");
}
}
num変数にvolatileを追加すると、実行図は次のようになります。
[外部リンク画像の転送に失敗しました。元のサイトにヒル防止リンクメカニズムがある可能性があります。画像を保存して直接アップロードすることをお勧めします(img-MPzPRjCX-1615980357906)(エントリから詳細(9)までマルチスレッド)- Javaメモリモデルとvolatile.assets / image-20201217100754170.png)
2.2アトミシティは保証されません
Atomicity:不可分、完全性、つまり、スレッドが特定のビジネスを実行している場合、スレッドを途中でブロックまたは分割することはできず、同時に完了するか、同時に失敗します。
揮発性は原子性を保証するものではなく、次のコードで検証できます。
class MyData2{
volatile int num = 0;
public void add(){
num++;
}
}
public class VolatileDemo2 {
public static void main(String[] args) {
MyData2 myData = new MyData2();
for(int i = 1;i <= 20;i++){
new Thread(() -> {
for (int j = 0; j < 2000; j++) {
myData.add();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(myData.num);
}
}
上記のコードは20個のスレッドを作成し、各スレッドはaddメソッドを2000回実行します。volatileがアトミック性を保証できる場合、最終結果は40,000になります。
答えは間違っています。volatileは原子性を保証しないことが確認できます。上記のコードを分析します
2つのスレッドが同時にメインメモリに書き込む場合、スケジューリングの問題により、1つのスレッドのみがメインメモリに書き込みに入ることができ、他のスレッドがウェイクアップしてメインスレッドに書き込むと、それは最新のデータではなくなります。これにより書き込みの上書きが発生し、最終結果は40000ではなくなります
どのように対処しますか?
- ロック、同期ロックの追加は、原子性の問題を確実に解決します
- JUCでAtomicIntegerアトミッククラスを使用する
class MyData2{
volatile int num = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void add(){
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo2 {
public static void main(String[] args) {
MyData2 myData = new MyData2();
for(int i = 1;i <= 20;i++){
new Thread(() -> {
for (int j = 0; j < 2000; j++) {
myData.add();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(myData.atomicInteger);
}
}
2.3注文の再配置の禁止
プログラム実行中のパフォーマンスを向上させるために、コンパイラーとプロセッサーはしばしば命令を並べ替えます。再注文には3つのタイプがあります。
-
コンパイラーに最適化された並べ替え。コンパイラーは、シングルスレッド・プログラムのセマンティクスを変更することなく、ステートメントの実行順序を再配置できます。
-
命令レベルの並列並べ替え。最新のプロセッサは、命令レベルの並列性(命令レベルの並列性、ILP)を使用して複数の命令をオーバーラップさせます。データの依存関係がない場合、プロセッサはマシン命令に対応するステートメントの実行順序を変更できます。
-
メモリシステムの並べ替え。プロセッサはキャッシュと読み取り/書き込みバッファを使用するため、ロードとストアの操作が順不同で実行されているように見えます。
プロセッサは、並べ替えるときに命令間の違いを考慮する必要がありますデータ依存性、例えば:
int x = 11; //①
int y = 12; //②
x = x + 5; //③
y = x * x; //④
コンパイラーが最適化する場合、データ依存性のため、ステートメント③はステートメント①の後になければなりません。
揮発性の禁止された命令再配置の最適化、マルチスレッド環境でのプログラムのアウトオブオーダー実行の現象を回避するために、主な原則は次のとおりです。
メモリバリア(メモリバリア)は、メモリバリアとも呼ばれ、CPU命令であり、次の2つの機能があります。
-
特定の操作の実行順序を確認し、
-
特定の変数のメモリの可視性を確保します(この機能を使用して揮発性メモリの可視性を実現します)。
コンパイラとプロセッサの両方が命令再配置の最適化を実行できるためです。メモリバリアが命令の間に挿入されると、コンパイラとCPUに、このメモリバリア命令では命令を並べ替えることができないことを通知します。つまり、メモリバリアを挿入することにより、メモリバリアの前後の命令は禁止されます。並べ替えの最適化を実行します。メモリバリアのもう1つの機能は、さまざまなCPUのキャッシュデータを強制的にフラッシュすることです。これにより、CPU上のすべてのスレッドがこれらのデータの最新バージョンを読み取ることができます。
2.4シングルトンモードで揮発性
シングルトンモードのDCLは次のように記述されます。二重検証
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo(){
}
public SingletonDemo getInstance(){
if(instance == null){
synchronized (SingletonDemo.class){
if(instance == null){
return new SingletonDemo();
}
}
}
return instance;
}
}
上記のコードが100万回実行され、エラーが1つだけ発生する可能性があります。その理由は、コンパイラの基になる命令が再配置されるためです。オブジェクトが新しいプロセスにある場合、次の4つのプロセスがあります。
1.クラスオブジェクトがロードされているかどうかを確認します。ロードされていない場合は、最初にクラスオブジェクトをロードします。
2.メモリスペースを割り当て、インスタンスを初期化します。
3.コンストラクターを呼び出します。
4.アドレスを参照に戻します
特定の実行において、上記の4つのステップのデータ依存性のために、命令を並べ替えて、②と④の順序を乱す可能性があります。つまり、アドレス参照が最初に返され、メモリスペースが後で割り当てられます。 。。たとえば、AとBの2つのスレッドがあります。最初にAスレッドが新しい操作を実行しますが、命令の再配置によって最初にアドレス参照が返されるため、Aスレッドが中断されただけであるのは残念です。このとき、Bスレッドはを取得します。 CPUリソース、==インスタンスの検索nullと等しくないため、メモリスペースを割り当てていない参照が直接返されます。==スレッドBに、まだ初期化されていない変数を使用させます。
、以下に約4つのプロセスがあります。
1.クラスオブジェクトがロードされているかどうかを確認します。ロードされていない場合は、最初にクラスオブジェクトをロードします。
2.メモリスペースを割り当て、インスタンスを初期化します。
3.コンストラクターを呼び出します。
4.アドレスを参照に戻します
特定の実行において、上記の4つのステップのデータ依存性のために、命令を並べ替えて、②と④の順序を乱す可能性があります。つまり、アドレス参照が最初に返され、メモリスペースが後で割り当てられます。 。。たとえば、AとBの2つのスレッドがあります。最初にAスレッドが新しい操作を実行しますが、命令の再配置によって最初にアドレス参照が返されるため、Aスレッドが中断されただけであるのは残念です。このとき、Bスレッドはを取得します。 CPUリソース、==インスタンスの検索nullと等しくないため、メモリスペースを割り当てていない参照が直接返されます。==スレッドBに、まだ初期化されていない変数を使用させます。
したがって、インスタンスにvolatileを追加して、命令の並べ替えを禁止する必要があります。