05-スレッド-Javaの並行性に関するその他の基本的な知識

Javaでのスレッドセーフの問題

共有リソースとは、リソースが複数のスレッドによって保持されていること、または複数のスレッドがリソースにアクセスできることを意味します。

スレッドセーフの問題とは、複数のスレッドが同期手段なしで共有リソースの読み取りと書き込みを同時に行うと、ダーティデータやその他の予期しない結果を引き起こす問題を指します。
ダーティリードとは、ソースシステムのデータが利用できなくなったことを意味します。範囲、実際のビジネスには意味がない、またはデータ形式が違法であり、ソースシステムに不規則なコーディングとあいまいなビジネスロジックがあります。

Javaの共有変数のメモリ可視性の問題

ここに画像の説明を挿入
Javaメモリモデルでは、すべての変数をメインメモリに格納することが規定されています。スレッドが変数を使用すると、メインメモリ内の変数が独自の作業スペースまたはワーキングメモリと呼ばれます。スレッドは、変数の読み取りと書き込みを行うときに自身で動作します。作業メモリー内の変数。

Javaメモリモデルでは、スレッドのローカルメモリ(作業メモリ)はCPUのL1およびL2キャッシュまたはレジスタに対応します。スレッドが共有変数を操作するとき、最初に共有変数をメインメモリから独自の作業メモリにコピーします。作業メモリーへの変更変数が処理され、処理後に変数値がメインメモリーに更新されます。このとき、キャッシュが存在するため、メモリが見えないという問題が発生します。

Javaでvolatileを使用すると、目に見えないメモリの問題を解決できます。

同期されたキーワード

同期ブロックは、Javaが提供するアトミックな組み込みロックです。Javaのすべてのオブジェクトを同期ロックとして使用できます。ユーザーに表示されないこれらの組み込みJavaロックは、内部ロックと呼ばれ、モニターロックの作成とも呼ばれます。スレッドの実行コードは、同期コードブロックに入る前に自動的に内部ロックを取得します。このとき、同期コードブロックにアクセスすると、他のスレッドはブロックされて一時停止されます。内部ロックを取得したスレッドは、同期コードブロックを正常に終了するか例外をスローした後、または組み込みロックリソースのwait seriesメソッドが同期ブロックで呼び出されたときに、組み込みロックを解放します。組み込みロックは排他ロックです。つまり、スレッドがロックを取得すると、他のスレッドはスレッドがロックを解放するのを待ってからロックを取得する必要があります。
また、Javaのスレッドはオペレーティングシステムのネイティブスレッドと1対1で対応しているため、スレッドがブロックされると、ユーザーモードからカーネルモードに切り替えてブロック操作を行う必要があります。非常に時間のかかる操作、および同期されたコンテキストスイッチの使用

同期されたメモリセマンティクス

同期ブロックに入るメモリセマンティクスは、同期ブロックで使用されている変数をスレッドの作業メモリからクリアすることです。そのため、変数が同期ブロックで使用されている場合、スレッドの作業メモリからは取得されません。 、ただしメインのGet inmemoryから直接。同期ブロックを終了するメモリセマンティクスは、同期ブロック内の共有変数の変更をメインメモリにフラッシュすることです。

これは、ロックとロック解除のセマンティクスでもあります。ロックが取得されると、ロックブロックのローカルメモリで使用される共有変数がクリアされます。これらの共有変数が使用されると、それらはからロードされます。メインメモリであり、ロックが解除されるとローカルメモリが解放されます。で変更された共有変数はメインメモリにフラッシュされます。共有変数メモリの可視性の問題を解決することに加えて、同期はアトミック操作を実現するためによく使用されます。また、synchronizedキーワードを使用すると、スレッドコンテキストが切り替わり、スレッドスケジューリングのオーバーヘッドが発生することにも注意してください。

揮発性キーワード

同期キーワードを使用して、ロックすることでメモリの可視性の問題を解決しますが、この方法は、スレッドコンテキストの切り替えのオーバーヘッドが発生するため、面倒です。メモリの可視性の問題については、Javaは、volatileキーワードを使用するという弱い形式の同期も提供します。

このキーワードにより、変数の更新が他のスレッドにすぐに表示されるようになります。変数が揮発性として宣言されている場合、スレッドは変数の書き込み時にレジスタまたは他の場所に値をキャッシュしませんが、値をメインメモリにフラッシュします。他のスレッドが共有変数を読み取ると、現在のスレッドの作業メモリー内の値を使用する代わりに、メインメモリーから最新の値を取得します。

volatileキーワードを使用してメモリの可視性の問題を解決する例を見てください。

//这是线程不安全的例子
public class ThreadNotSafeInteger{
    
    
	private int value;
	public int get(){
    
    
	return value;
	}
	public void set(int value){
    
    
	 this.value =value;
	}
}
//使用synchronized关键字进行同步的方式
public class ThreadNotSafeInteger{
    
    
	private int value;
	public synchronized int get(){
    
    
	return value;
	}
	public synchronized void set(int value){
    
    
	 this.value =value;
	}
}
//这是使用volatile关键字进行同步的方式
public class ThreadNotSafeInteger{
    
    
	private  volatile int value;
	public int get(){
    
    
	return value;
	}
	public void set(int value){
    
    
	 this.value =value;
	}
}

ここでの同期と揮発性の使用は同等であり、どちらも共有変数値のメモリ可視性の問題を解決しますが、前者は排他ロックであり、1つのスレッドのみが同時にget()メソッドを呼び出すことができ、他の呼び出しスレッドはブロックされると同時に、スレッドコンテキストの切り替えとスレッドの再スケジュールのオーバーヘッドが発生します。これは、ロックを使用する場合の欠点でもあります。後者は非ブロッキングアルゴリズムであり、スレッドコンテキスト切り替えのオーバーヘッドを引き起こしません。

ただし、すべてがすべての場合に使用できるわけではありません。volatileは可視性を保証しますが、操作のアトミック性を保証するものではありません。

どのような状況でvolatileキーワードを使用する必要がありますか?
1.変数値を書き込むときは、変数の現在の値に依存しません。現在の値に依存する場合、get-calculate-writeの3ステップの操作になるため、これらの3ステップの操作はアトミックではなく、volatileはアトミック性を保証しません。
2.変数値の読み取りおよび書き込み時にロックはありません。ロック自体はすでにメモリの可視性を保証しているため、現時点では変数を揮発性として宣言する必要はありません。

Javaでのアトミック操作

いわゆるアトミック操作とは、一連の操作を実行すると、これらの操作がすべて実行されるか、まったく実行されないことを意味し、一部だけが実行されることはありません。カウンタを設計するときは、通常、最初に現在の値を読み取り、次に+1してから、更新します。このプロセスはリードモディファイライトプロセスです。このプロセスがアトミックであることが保証されない場合、スレッドセーフの問題が発生します。次のコードはスレッドセーフではありません。++ valueがアトミック操作であるとは保証できないためです。

public class ThreadNotSafeCount{
    
    
	private Long value;

	public Long getCount(){
    
    
	return value;
	}

	public void inc(){
    
    
	++value;
}

}

Javap -cコマンドを使用してアセンブリコードを表示すると、++ valueが多くのステップに分割されて、操作が一緒に完了することがわかります。したがって、++ valueなどの最も単純なステートメントでは、アトミック操作を保証できません。

最も簡単な方法は、アトミック操作を保証できるsynchronizedキーワードを使用することです。

public class TreadSafeCount{
    
    
	private Long value;
	public synchronized Long getCount(){
    
    
	return value;
	}

	public synchronized void inc(){
    
    
	++value;
}

同期キーワードを使用すると、スレッドセーフ、つまりメモリの可視性とアトミック性を実際に実現できますが、同期は排他ロックであり、内部ロックを取得していないスレッドはブロックされます。ここでのgetCountメソッドは単なる読み取り操作であり、複数スレッドは同時に呼び出されますスレッドの安全性の問題はありませんが、synchronziedキーワードを追加すると、同時に呼び出すことができるスレッドは1つだけになるため、同時実行性が大幅に低下します。これは読み取り操作にすぎないので、getCountメソッドのsynchronizedキーワードを削除してみませんか?実際、削除することはできません。値のメモリの可視性を実現するために、ここで同期をテストすることを忘れないでください。

ノンブロッキングCASアルゴリズムによって実装されたAtomicLongを使用して、内部でアトミック操作を実行することをお勧めします。

CAS操作

Javaでは、ロックは並行処理の場所を占めますが、ロックを使用することには欠点があります。つまり、サイトがロックを取得しない場合、ロックはブロックされて一時停止され、スレッドコンテキストの切り替えとオーバーヘッドの再スケジュールにつながります。Javaは、共有変数の可視性の問題を解決するための非ブロッキングvolatileキーワードを提供します。これは、特定のプログラムのロックによって引き起こされるオーバーヘッドを補います。ただし、volatileは共有変数の可視性を保証するだけであり、読み取り-変更-書き込みのアトミックな問題を解決することはできません。CASは、JDKによって提供される非ブロッキングアトミック性であり、ハードウェアを介した比較更新操作のアトミック性を保証します。JDKのUnsafeクラスは、一連のcomapreAndSwapメソッドを提供します。

CASを使用しても、プログラムの正しい実行が保証されない場合があります。CAS操作には古典的なABA問題があります。

ABA問題は、CAS操作中に、他のスレッドが変数値AをBに変更するが、Aに戻すことを意味します。このスレッドが期待値Aを使用して現在の変数と比較すると、変数Aが見つかります。が変更されていないため、CASが変更されますAの値が交換されましたが、実際には値が他のスレッドによって変更されており、楽観的ロックの設計アイデアと矛盾しています。ABA問題の解決策は、変数が更新されるたびに変数のバージョン番号に1を追加することです。そうすると、ABAはA1-B2-A3になります。変数がスレッドによって変更される限り、バージョン番号はに対応します。変更は増分変更が発生し、それによってABA問題が解決されます。

安全でないクラス

JDKのrt.jarパッケージのUnsafeクラスは、ハードウェアレベルのアトミック操作を提供します。Unsafeクラスのメソッドはすべてネイティブメソッドであり、JNIを使​​用してローカルのC ++実装ライブラリにアクセスします。

資料を読む:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

Java命令の並べ替え

Javaメモリモデルを使用すると、コンパイラとプロセッサは命令を並べ替えて動作パフォーマンスを向上させ、データの依存関係がない命令のみを並べ替えることができます。シングルスレッドで並べ替えると、最終的な実行結果がプログラム実行の結果と一致するようになりますが、マルチスレッドでは問題が発生します。

volatileキーワードを使用すると、命令の並べ替えやメモリの可視性の問題を回避できます。

ロック

楽観的および悲観的なロック

楽観的ロックと悲観的ロックはデータベースで導入された用語ですが、並行パッケージロックでも同様のアイデアが導入されています。
ペシミスティックロックとは、データが他のスレッドによって簡単に変更されるため、データが処理される前にデータがロックされ、データ処理プロセス全体でデータがロックされるという、外界によって変更されるデータに対する保守的な態度を指します。
オプティミスティックロックは比較的悲観的なロックです。データは通常の状況では競合を引き起こさないと考えられているため、レコードにアクセスする前に排他的にロックされるのではなく、データが送信および更新されるときにデータが競合するかどうかを正式にチェックします。検出。
ペシミスティックロックの実現は、多くの場合、データベースによって提供されるロックメカニズムに依存しています。
オプティミスティックロックは、データベースが提供するロックメカニズムを使用しません。通常、バージョンフィールドをテーブルに追加するか、ビジネスステータスを使用して実装されます。オプティミスティックロックは、コミットするまでロックされないため、デッドロックは発生しません。

フェアロックとアンフェアロック

サイトでロックを取得するプリエンプションメカニズムによれば、ロックはフェアロックとアンフェアロックに分けることもできます。フェアロックは、サイトでロックを取得する順序が、スレッドがロックを要求する時間によって決定されることを示します。最初にロックを要求するスレッドは、最も早くロックを取得します。ロックに対して。不公平なロックは実行時に侵入します。つまり、先着順であり、必ずしも先着順であるとは限りません。
ReetrantLockは、公正および不公正なロックの実現を提供します。
フェアロック:ReetrantLock fairlock = new ReetrantLock(true)
不公平ロック:ReetrantLock fairlock = new ReetrantLock(false0。コンストラクターが渡されない場合、デフォルトは不公平ロック
です。公平性ロックはパフォーマンスをもたらすため、公平性要件なしで不公平ロックを使用してみてくださいオーバーヘッド。

排他ロックと共有ロック

ロックを保持できるのは単一のスレッドだけか複数のスレッドかによって、ロックは排他ロックと共有ロックに分けることができます。
排他的ロックは、一度に1つのサイトのみがロックを取得できることを保証し、ReetrantLockは排他的な方法で実装されます。共有ロックは、ReadWriteLockなど、複数のスレッドで同時に保持できます。これにより、リソースを複数のスレッドで同時に読み取ることができます。

排他ロックは一種の悲観的ロックです。これは、リソースへの各アクセスが相互排他ロックに追加されるため、同時実行が制限されます。これは、読み取り操作がデータの整合性に影響を与えず、排他ロックが許可するスレッドが1つだけであるためです。同時にデータを読み取るには、他のスレッドは現在のスレッドがロックを解放するのを待ってから読み取る必要があります。
共有ロックは楽観的ロックであり、ロック条件を拡大し、複数のスレッドが同時に読み取り操作を実行できるようにします。

リエントラントロック

スレッドが別のサイトで保持されている排他ロックを取得しようとすると、スレッドはブロックされますが、スレッドがすでに取得したロックを再度取得すると、ブロックされますか?ブロックされていない場合は、ロックを再入可能にロックします。つまり、スレッドがロックを取得している限り、ロックされたコードブロックにほぼ無数に入ることができます。

どのような状況でリエントラントロックが使用されるかの例を見てみましょう。

public class Hello{
    
    
		public synchronized void helloA(){
    
    
		System.out.println("hello");
	}
	public synchronized void helloB(){
    
    
	System.out.println("hello B");
	helloA();
}
}

上記のコードでは、helloBメソッドを呼び出す前に組み込みロックが取得され、出力が出力されます。helloAメソッドを呼び出した後、呼び出しの前に組み込みロックが取得されます。組み込みロックが再入可能でない場合、呼び出しスレッドは常にブロックされます。
実際、同期された内部ロックは再入可能ロックです。再入可能ロックの原則は、ロック内にスレッドマークを維持して、ロックが現在占有されているスレッドを示し、カウンターを関連付けることです。カウンタは最初は0であり、ロックがどのスレッドによっても占有されていないことを示します。スレッドがロックを取得すると、カウンターの値は1になります。他のスレッドが再びロックを取得すると、ロックの所有者は自分自身ではなく、ブロックされて一時停止されていることがわかります。

スピンロック

Javaのスレッドはオペレーティングシステムのスレッドに1対1で対応しているため、シーンがロック(排他ロックなど)の取得に失敗すると、カーネル状態に切り替えられて一時停止されます。スレッドがロックを取得すると、カーネル状態に切り替えてスレッドをウェイクアップする必要があります。ただし、ユーザー状態からカーネル状態への切り替えのオーバーヘッドは非常に大きく、特定のプログラムの同時パフォーマンスに影響します。スピンロックとは、現在のスレッドがロックを取得するときに、ロックがすでに他のスレッドによって占有されていることを検出した場合、すぐにブロックされないことです。CPUを使用する権利を放棄せずに、複数のスレッドを取得しようとします。回数(デフォルト数は10で、使用できます)-XX:PreBlockSpinshパラメーターがこの値を設定します)、次の数回の試行で他のスレッドがロックを解放した可能性が非常に高くなります。指定された回数の試行後にロックが取得されない場合、現在のスレッドはブロックされて中断されます。このことから、スピンロックはスレッドのブロックとスケジューリングのオーバーヘッドと引き換えにCPU時間を使用していることがわかりますが、このCPU時間は無駄になる可能性が非常に高くなります。

おすすめ

転載: blog.csdn.net/qq_41729287/article/details/113941606