i ++はアトミック操作ではないため、スレッドセーフ操作ではありません。
それで、i ++と同様にこの効果を達成したい場合、どのコレクションまたはツールクラスを使用する必要がありますか?
JDK1.5より前は、マルチスレッドで特定の基本
データ型または引用
データ型の操作のアトミック性を確保するために、外部キーワードsynchronized
に依存する必要がありますが、この状況はJDK1.5以降変更されました。もちろん、同期を使用することもできます。アトミック性を確保するために、ここで話しているスレッドセーフメソッドの1つは、AtomicInteger、AtomicBooleanなどのアトミックツールクラスです。これらのアトムクラスはスレッドセーフなツールであり、それらもそうLock-Free
です。これらのツールとロックフリーの概念を見てみましょう。
AtomicIntegerを理解する
AtomicInteger
これはJDK 1.5で新しく追加されたツールクラスです。最初に、その継承関係を見てみましょう
ラッパークラスInteger intのように、それらはNumber
クラスから継承されます。
Numberクラスは、基本的なデータ型のラッパークラスであり、通常、データ型に関連するオブジェクトはNumberクラスから継承します。
その継承システムは非常にシンプルです。基本的なプロパティとメソッドを見てみましょう
AtomicIntegerの基本的なプロパティ
AtomicIntegerには3つの基本的な属性があります。
Unsafe
sun.misc
ネイティブメソッドの数に主に依存パッケージ以下のクラス、のAtomicIntegerは操作アトミック保証を提供sun.misc.Unsafe。
UnsafeこのobjectFieldOffset
メソッドは、オブジェクトメモリアドレスオフセットに関して、メモリメンバープロパティのアドレスを取得する場合があります。簡単に言えば、メモリ内でこの変数のアドレスを見つけます。これは、メモリアドレスを介して直接後続の操作を行うのに便利です。この値はvalue
これについては後で詳しく説明します
value
AtomicInegerの値です。
AtomicIntegerの構築方法
続けて見てみると、AtomicIntegerの構築メソッドは2つしかありません。1つはパラメーターなしの構築メソッドです。パラメーターなしの構築メソッドのデフォルト値は0です。パラメーター化された構築メソッドは初期値を指定できます。
AtomicIntegerのメソッド
AtomicIntegerのメソッドについて話しましょう。
取得して設定
まず、最も簡単なgetメソッドとsetメソッドを見てみましょう。
get()
:AtomicIntegerの現在の値を取得します
set()
:AtomicIntegerの現在の値を設定します
AtomicIntegerのアトミックデータはget()で読み取ることができ、set()は現在のアトミック値を設定できます。これは、get()およびset()が最終的に変数値に適用され、値がvolatile
変更されるためです。 getおよびsetは、メモリの読み取りおよび設定と同等です。以下に示すように
上記のi ++およびi ++の非アトミック操作について説明しましたが、AtomicIntegerのメソッドを使用して置き換えることができると述べました。
増分演算
Incremental
私たちのニーズを満たすための適切な方法でのAtomicInteger
getAndIncrement()
:現在の値を原子的に増やし、結果を返します。同等のi++
操作。
スレッドセーフかどうかを確認するために、次の例を使用してテストします
public class TAtomicTest implements Runnable{
AtomicInteger atomicInteger = new AtomicInteger();
@Override
public void run() {
for(int i = 0;i < 10000;i++){
System.out.println(atomicInteger.getAndIncrement());
}
}
public static void main(String[] args) {
TAtomicTest tAtomicTest = new TAtomicTest();
Thread t1 = new Thread(tAtomicTest);
Thread t2 = new Thread(tAtomicTest);
t1.start();
t2.start();
}
}
結果を出力することで、スレッドセーフな操作であることがわかります。iの値は変更できますが、最終的に結果はi-1のままです。これは、最初に値が取得され、次に+ 1が取得されるため、その概略図は次のとおりです。
incrementAndGet
これに対して、+ 1演算が先に実行され、その後、インクリメントの結果が返されるため、この演算メソッドは値のアトミック演算を保証できます。以下に示すように
デクリメンタル操作
対照的に、x--やx = x-1などのデクリメント操作もアトミックです。AtomicIntegerのメソッドを引き続き使用して、
getAndDecrement
:現在の型のint値を返し、valueの値を減らします。以下はテストコードです
class TAtomicTestDecrement implements Runnable{
AtomicInteger atomicInteger = new AtomicInteger(20000);
@Override
public void run() {
for(int i = 0;i < 10000 ;i++){
System.out.println(atomicInteger.getAndDecrement());
}
}
public static void main(String[] args) {
TAtomicTestDecrement tAtomicTest = new TAtomicTestDecrement();
Thread t1 = new Thread(tAtomicTest);
Thread t2 = new Thread(tAtomicTest);
t1.start();
t2.start();
}
}
以下はgetAndDecrementの概略図です。
decrementAndGet
:同様に、decrementAndGetメソッドは、最初にデクリメント操作を実行し、次にvalueの値を取得します。図は次のとおりです
LazySetメソッド
揮発性には記憶バリアがあることを知っていますか?
メモリの障壁とは何ですか?
メモリバリアは、
内存栅栏
メモリバリア、バリア命令などとも呼ばれ、CPUまたはコンパイラによるメモリへのランダムアクセス操作の同期ポイントである同期バリア命令の一種であり、このポイントより前のすべての読み取りおよび書き込み操作が実行されます。その後、この時点以降に操作を開始できます。また、CPUプロセッシングユニットのメモリ状態を他のプロセッシングユニットから見えるようにするテクノロジーでもあります。
CPUは、キャッシュの使用、命令の再配置など、多くの最適化を使用します。最終的な目標はパフォーマンスです。つまり、プログラムが実行されるとき、最終結果が同じであれば、命令が再配置されるかどうかは問題ではありません。そのため、命令の実行タイミングが順番に実行されるのではなく、順番が乱れて実行されるため、多くの問題が発生し、メモリバリアの出現を助長します。
意味的には、メモリバリアの前のすべての書き込み操作はメモリに書き込む必要があります。メモリバリアの後の読み取り操作では、同期バリアの前に書き込み操作の結果を取得できます。したがって、機密性の高いプログラムブロックの場合、書き込み操作の後、読み取り操作の前にメモリバリアを挿入できます。
メモリバリアのオーバーヘッドは非常に軽量ですが、それがどれほど小さくてもオーバーヘッドがあります。LazySetはこれを正確に実行します。通常の変数の形式で変数を読み書きします。
それはまた言うことができます:障壁を設定するのが面倒
GetAndSetメソッド
原子的に指定された値に設定し、古い値を返します。
そのソースコードは、以下に示すように、安全でない状態でgetAndSetIntメソッドを呼び出すことです。
最初に配布され、次にgetIntVolatile
cppで見つけられなかったメソッドを呼び出します。見つけるために小さなパートナーが覚えておくように教えてください。
compareAndSwapIntがfalseを返すまでループします。つまり、CASは新しい値に更新されないため、var5は最新のメモリ値を返します。
CAS法
CASは実際にはCompareAndSet
その名前が示すように、もちろん意味を比較および更新する方法であるとよく言われますが、これは文字通り、文字通り少しの逸脱です。実際、人々は比較して、満足すれば更新されなくなります。
上記のCAS Javaソースコードレベルの場合、JDKは、現在の値がexpectの値と等しい場合、現在の値のアトミックな性質が指定された値を更新するように設定され、このメソッドがブール型を返すことを公式に説明します。 trueの場合は、比較と更新が成功したことを意味し、それ以外の場合は失敗したことを意味します。
CASはロックフリーの同時実行メカニズムでもあり、CASとも呼ばれLock Free
ます。ロックフリーは非常に大きいと思いますか。どういたしまして。
以下では、ロックとロック解除を構築します CASLock
class CASLock {
AtomicInteger atomicInteger = new AtomicInteger();
Thread currentThread = null;
public void tryLock() throws Exception{
boolean isLock = atomicInteger.compareAndSet(0, 1);
if(!isLock){
throw new Exception("加锁失败");
}
currentThread = Thread.currentThread();
System.out.println(currentThread + " tryLock");
}
public void unlock() {
int lockValue = atomicInteger.get();
if(lockValue == 0){
return;
}
if(currentThread == Thread.currentThread()){
atomicInteger.compareAndSet(1,0);
System.out.println(currentThread + " unlock");
}
}
public static void main(String[] args) {
CASLock casLock = new CASLock();
for(int i = 0;i < 5;i++){
new Thread(() -> {
try {
casLock.tryLock();
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}finally {
casLock.unlock();
}
}).start();
}
}
}
上記のコードでは、CASLockを作成しました。tryLock
メソッドでは、最初にCASメソッドの更新を使用します。例外がスローされ、現在のスレッドがロックスレッドに設定されている場合、更新は成功しません。このunLock
メソッドでは、まず、現在の値が0かどうかを判断します。直接戻りを確認したい結果である場合は0です。それ以外の場合は、現在のスレッドがまだロックされていることを意味しますので、現在のスレッドがロックされているかどうかを判断し、ロックされている場合は、ロック解除操作を行います。
次に、前述のcompareAndSetは、実際には次の操作として解析できます。
// 伪代码
// 当前值
int v = 0;
int a = 0;
int b = 1;
if(compare(0,0) == true){
set(0,1);
}
else{
// 继续向下执行
}
生活シーンでチケットを購入する例をとることもできます。景勝地に入るにはチケットを持っている必要があります。景勝地に出会えない偽のチケットやチケットがあれば、確実に識別できます。あなたは確かに景勝地に入ることはできません。
ナンセンスな話をやめてください。compareAndSetの概略図です。
weakCompareAndSet
:くそー、私は何度か注意深く見ましたが、JDK1.8のこのメソッドは、compareAndSetメソッドとまったく同じであることがわかりました。。。
しかし、これは本当ですか?いいえ、JDKのソースコードは非常に広範で深遠なので、反復的な方法を設計することはありません。JDKチームがそのような低レベルのチームをコミットすることはありませんが、その理由は何ですか?
「Java High Concurrency Detailed」という本は私たちに答えを与えてくれます
AddAndGet
AddAndGet、getAndIncrement、getAndAdd、incrementAndGet、およびその他のメソッドはすべて、do ... while + CAS操作を使用します。これは実際にはスピンロックに相当します。CASの変更が成功すると、ループが継続し、変更の失敗が返されます。回路図は次のとおりです
AtomicIntegerに飛び込む
上記でAtomicIntegerの具体的な使用法について説明しましたが、AtomicIntegerは、原子性を保証するためにvolatileおよびCASに依存していることがわかっているので、なぜCASが原子性を保証できるのか、およびその下層は何かを分析しましょう。AtomicIntegerは楽観的ロックとどのような関係がありますか?
AtomicIntegerの基礎となる実装原理
compareAndSetL(CAS)
この2行のコードが原子性を保証するこの素敵な方法を見てみましょう。
このメソッドは、安全でないcompareAndSwapInt
メソッドでCASを呼び出すことと同等であることがわかります。実装に送信できるのは、安全でないルックのみです。
compareAndSwapIntはsun.misc
メソッドであり、これはnative
メソッドであり、達成するための基礎となるC / C ++であるため、C / C ++ソースコードを確認する必要があります。
C / C ++の素晴らしさを知っていますか?Javaを使用すると、アプリケーションとアーキテクチャが再生され、C / C ++はサーバーと最下層を再生します。
jdk8u-dev/hotspot/src/share/vm/prims/unsafe.app
パス内のcompareAndSwapIntソース、そのソースは達成されました
それはUnsafe_CompareAndSwapInt
私たちがこの方法を見つけた方法です
C / C ++のソースコードは理解できませんが、キーコードを見つけることができAtomic::cmpxchg
ます。cmpxchgはx86 CPUアーキテクチャのアセンブリ命令です。主な機能は、オペランドの比較と交換です。この命令の定義を探し続けましょう。
異なるosに対応し、基礎となる実装が異なることがわかります
次のようにWindowsの実装を見つけました
引き続き調べていきますが、実際には216行目のコードを定義しています。
このとき、組立説明書やレジスターの知識が必要です。
上記os::is-MP()
はマルチプロセッシングオペレーティングシステムのインターフェースで、以下は__asmです。これは、インラインアセンブラを呼び出すために使用されるC / C ++キーワードです。
__asmアセンブラーコードは、大まかに言うと、dest、exchange_value、compare_value値がレジスターに配置されLOCK_IF_MP
、コードの一般的な意味は次のとおりです。
マルチプロセッサの場合、ロックを実行してから比較操作を実行します。比較が示すMPはMultiProcess
、MP でje
表され、等ジャンプで表されます。L0 はフラグを表します。
上記の組み立て手順に戻ります。CASが基本的なcmpxchg
手順です。
楽観的ロック
AtomicIntegerは、現在の値を取得することができますなぜあなたはこの質問を持っていますか、なぜまだあるexpectValue
とvalue
、それの矛盾?
AtomicIntegerはツールの1つのアトムにすぎないため、排他的ではなく、相互に排他的で排他的であるのと同じsynchronized
またはlock
同じではありません。AtomicIntegerを取得して設定するには2つの方法があることに注意してください。volatile
少し変更されたものを使用しますが、atomic volatileはありません。そのため、現在の値とexpectValueの値に一貫性がなく、変更が繰り返される可能性があります。
上記の2つの状況には解決策があり、1つはと同様のロックメカニズムを使用synchronized
してlock
います。これは、排他ロックを備えています。つまり、この方法で変更されるスレッドは一度に1つだけです。原子性ですが、比較的高価です。この種のロックは、悲観的ロックとも呼ばれます。別の解決策は、使用する版本号
か、はいCAS 方法
です。
バージョンナンバー
バージョン番号は、version
達成するフィールドを追加するデータテーブルメカニズムにあり、書き込み操作が実行されて書き込みが成功したときにデータの数が変更されたことを示します。バージョン=バージョン+ 1、スレッドAがデータを更新するときデータは同時にバージョン値も読み取ります。更新を送信するときに、読み取ったばかりのバージョン値が現在のデータベースのバージョン値と等しい場合は、更新されます。それ以外の場合は、更新が成功するまで更新操作が再試行されます。
CAS法
もう1つの方法はCASです。ここでは、CASメソッドを紹介するために上記の多くのスペースを使用しましたが、その操作メカニズムについてある程度理解していると思いますが、その操作メカニズムについては詳しく説明しません。
すべてに長所と短所があります。ソフトウェア業界には完璧な解決策はありませんが、最良の解決策があります。そのため、Optimistic Lockにも弱点と欠陥、つまりABA問題があります。
ABAの問題
ABA問題は、最初に読み取られた変数の値がAであり、Aに書き込む準備ができているときに、値がまだAであることがわかった場合に、この場合、Aの値が変更されていないと見なすことができると述べています。 ?A-> B-> Aの場合もありますが、AtomicIntegerはそうは思わず、見ているものだけを信じ、見ているものは見ているものです。例えば
下の図に示すように、単一にリンクされたリストがある場合
A.next = BおよびB.next = null。この時点で、2つのスレッドT1およびT2は、それぞれシングルリンクリストからAを取り出します。いくつかの特別な理由により、T2はAをBに変更し、次にAに変更します。このとき、T1 CASメソッドを実行して、片方向リンクリストがまだAであることを確認し、CASメソッドを実行します。結果は正しいですが、この操作はいくつかの潜在的な問題を引き起こします。
この時点では、まだ単一リンクリストです。2つのスレッドT1とT2がそれぞれ単一リンクリストからAを取り出し、T1は次の図に示すようにリンクリストをACDに変更します。
この時間T2で、メモリ値がまだAの場合、Bの参照がnullであるため、Aの値をBに置き換えようとします。CとDはフリー状態になります。
AtomicStampedReference
クラスの後のJDK 1.5 はそのような機能を提供します。compareAndSet
メソッドは最初に現在の値が期待値と等しいかどうかをチェックし、現在の参照標準が決定され、期待される参照のしるしとしるしが等しく、すべてが等しい場合は原子的に設定します。指定された更新値です。
上記はJavaコードフローです。ネイティブを見ると、再びcppを実行することがわかります。カイル
簡単な説明はUnsafeWrapper
、そのラッパー、名前の変更のみです。次に、JNIの処理の後で、compareAndSwapOjectは参照で比較するため、C ++オブジェクト指向の変換を行う必要があります。主な方法はatomic_compare_exchange_oop
ご覧のとおり、おなじみの語彙が再び登場しましたcmpxchg
。つまり、compareAndSwapOjectは引き続きcmpxchgアトミック命令を使用していますが、一連の変換が行われています。
追記
質問が提起され、CASは変数間の可視性を保証できますか?どうして?
別の質問がありgetIntVolatile
ます。メソッドのcppソースコードはどこにありますか?見つけ方?
トップボスがこれら2つの問題に関心を持っている場合は、連絡してください。
公式アカウントプログラマーcxuanをフォローし、cxuanに返信して高品質の情報を受け取ります。
私は6つのPDFを自分で書きました。非常にハードコアで、リンクは次のとおりです。
私は6つのPDFを自分で書きました。非常にハードコアで、リンクは次のとおりです。
私は6つのPDFを自分で書きました。非常にハードコアで、リンクは次のとおりです。