第8章 CAS戦略

JDKが提供するアトミッククラス

1671202811439.jpg

  • jdk5 は並行パッケージ java.util.concurrent.* を追加し、次のクラスは CAS アルゴリズムを使用して同期同期ロックとは異なる楽観的ロックを実装します。JDK 5 より前の Java 言語は、同期を確実にするために synchronized キーワードに依存していました (これは排他的ロックと悲観的ロックです)。

アトミックの原理

  • Atomic パッケージのクラスの基本的な特徴は、マルチスレッド環境では、複数のスレッドが 1 つの変数 (基本型と参照型を含む) を同時に操作する場合、それらは排他的になることです。つまり、複数のスレッドが同時に変数の値を操作します。更新する場合、成功できるのは 1 つのスレッドだけであり、失敗したスレッドは実行が成功するまでスピン ロックのように試行を続けることができます。

CAS前

  • マルチスレッド環境では、スレッドの安全性を確保するためにアトミック クラスを使用しません。 i++ (基本データ型)

一般的に使用されるsynchronizedロックですが、比較的重く、ユーザー モードとカーネル モードの切り替えが必要となるため、効率的ではありません。

public class CASDemo1 {
    
    
	//利用volatile
    private volatile int num;

    public int getNum() {
    
    
        return num;
    }

    public synchronized void setNum(int num) {
    
    
        num++;
    }
}

CAS導入後

public class CASDemo1 {
    
    
  
    AtomicInteger atomicInteger = new AtomicInteger();
    public int getNum() {
    
    
        return atomicInteger.get();
    }

    public void setNum(int num) {
    
    
        atomicInteger.getAndIncrement();
    }
}

CASとは

  • Compare and Swap は実際にはスレッドをブロックせず、常に更新を試みる楽観的ロックの実装方法です。

  • 中国語訳は比較と交換で、並列アルゴリズムを実装するときに一般的に使用されるテクノロジです。これには、メモリ位置、期待される元の値、および更新された値の 3 つのオペランドが含まれます。

    • CAS 操作を実行するときは、メモリ位置の値を予想される元の値と比較します。

    • 一致するものがあれば、プロセッサは位置の値を新しい値に自動的に更新します。

    • 不一致がある場合、プロセッサはいかなる操作も実行せず、同時に CAS 操作を実行する複数のスレッドのうち 1 つだけが成功します。

CASの原理

  • CAS には、位置メモリ値 V、古い期待値 A、および変更される更新値 B の 3 つのオペランドがあります。
    • 古い期待値 A とメモリ値 V が同じ場合にのみ、メモリ値 V を B に変更します。そうでない場合は、何も行わないか、最初からやり直します。
      • 戻ってきたときに再試行するこの動作は、「スピン!」になります。
画像-20230615183132937 画像-20230615183353242 画像-20230615183114477
  • スレッド 2 の操作が失敗しました。スピン操作を実行し、メイン メモリ内の値を再度読み取り、再試行します。

コードデモ

public class CASDemo2 {
    
    
    public static void main(String[] args) {
    
    
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,200)+"\t"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5,300)+"\t"+atomicInteger.get());
    }
}
//true	200
//false 200

原子性を確保する方法

  • スレッド 1 の A が 0 であると判断した後、その新しい値を更新しようとしたときに、他のスレッドによって i の値が変更された可能性はありますか?
    • しない。CAS はアトミックな操作であるため、システム プリミティブであり、CPU のアトミックな命令であり、CPU レベルからそのアトミック性が保証されます。
      • 言い換えれば、比較および交換操作は成功するか失敗するかのどちらかです。

ハードウェアレベルの保証

  • バスをロックする方が、同期するよりも効率的です。

CAS は JDK によって提供され非阻塞原子性操作、ハードウェアによる比較更新のアトミック性を保証します。

  • これはノンブロッキングかつアトミックであるため、効率が高く、ハードウェアによって保証されており、信頼性が高くなります。

CAS は CPU ( cmpxchg指令) のアトミックな命令であり、いわゆるデータの不整合の問題を引き起こしません。Unsafe提供されるCAS方法基本的な実装(compareAndSwapXXX など) は CPU 命令 cmpxchg です。

  • cmpxchg 命令が実行されると、現在のシステムがマルチコア システムであるかどうかが判断され、マルチコア システムであるバスがロックされます。1つのスレッドのみがバス ロックに成功します。ロックが成功すると、CAS 操作が実行されます。実行される。
    • 言い換えれば、CAS のアトミック性は実際には CPU によって実装されます。実際、この時点ではまだ排他ロックが存在しますが、ここでの排他時間は同期を使用するよりもはるかに短いため、マルチスレッドでのパフォーマンスは向上します。状況。

覚えておく必要があるのは、CAS はハードウェア レベルでの効率を向上させるためにハードウェアによって実装されているということです。最下層は引き続きハードウェアに任せて、原子性と可視性を確保します。

実装はハードウェア プラットフォームのアセンブリ命令に基づいており、Intel の CPU (X86 マシン上) ではアセンブリ命令 cmpxchg 命令が使用されます。

中心的な考え方は、更新する変数の値 V を期待値 E と比較する (比較) ことです。それらが等しい場合、V の値は新しい値 N に設定されます (交換)。等しくない場合は、 、再び回転します。

ソースコード分析

//compareAndSet
//发现它调用了Unsafe类
public final boolean compareAndSet(int expect, int update) {
    
    
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//compareAndSwapInt
//发现它调用了native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//这三个方法是类似的
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上記の 3 つの方法は似ていますが、主に 4 つのパラメータについて説明します。

  • var1: 操作対象のオブジェクトを表します
  • var2: 操作対象のオブジェクト内の属性アドレスのオフセットを表します。
  • var4: データを変更する必要がある期待値を表します。
  • var5/var6: 変更する必要がある新しい値を示します。

ここで、「 Unsafeクラスとは何ですか?」という疑問が生じます。

CAS の基本原理は何ですか? ご存知の場合は、UnSafe についての理解を教えてください

public class AtomicInteger extends Number implements java.io.Serializable {
    
    
    private static final long serialVersionUID = 6214790243416807050L;

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
static {
    
    
    try {
    
    
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) {
    
     throw new Error(ex); }
}

private volatile int value;//保证变量修改后多线程之间的可见性
}
  • 空を飛ぶというコンセプトは地上でも実現しなければならない

    • CAS の概念は Unsafe クラスに実装されています
  • これは CAS のコア クラスです。Java メソッドは基盤となるシステムに直接アクセスできず、ネイティブ メソッドを通じてアクセスする必要があるため、Unsafe はバックドアに相当します。このクラスに基づいて、特定のメモリ内のデータを直接操作できます。Unsafe クラスは sun.misc パッケージに存在し、Java での CAS 操作の実行は Unsafe クラスのメソッドに依存するため、その内部メソッド操作は C ポインターと同様にメモリを直接操作できます。

    • Unsafe クラスのメソッドはすべてネイティブに変更されていることに注意してください。これは、Unsafe クラスのメソッドがオペレーティング システムの基礎となるリソースを直接呼び出して、対応するタスクを実行することを意味します。

変数値オフセット

  • Unsafe はメモリ オフセット アドレスに基づいてデータを取得するため、メモリ内の変数値のオフセット アドレスを示します。
public final int getAndIncrement() {
    
    
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
 valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value"));

変数値は volatile で変更されます

  • 可視性を実現するために value 値は揮発性であり、スレッドが比較のためにメイン メモリの最新の値をリアルタイムで取得できるようにします。

例 atomicInteger.getAndIncrement() が安全な理由

  • CAS の正式名は Compare-And-Swap で、CPU 同時実行プリミティブです。
    • その機能は、メモリ内の特定の位置の値が期待値であるかどうかを判断し、期待値である場合はそれを新しい値に変更することであり、このプロセスはアトミックです。
  • AtomicInteger クラスは主に CAS (比較およびスワップ) + 揮発性およびネイティブ メソッドを使用してアトミックな操作を保証するため、同期による高いオーバーヘッドを回避し、実行効率を大幅に向上させます。

画像-20230703102558402

CAS 同時実行プリミティブは、sun.misc.Unsafe クラスのさまざまなメソッドとして JAVA 言語で具体化されます。UnSafe クラスの CAS メソッドを呼び出すと、JVM が CAS アセンブリ命令の実装を支援します。これは完全にハードウェアに依存する機能で、アトミックな操作が実装されます。繰り返しになりますが、CAS はシステム プリミティブであるため、プリミティブはオペレーティング システム用語のカテゴリに属し、複数の命令で構成されています。特定の機能を完了するために使用されるプロセスであり、プリミティブの実行は継続的である必要があります。割り込みは発生しません。つまり、CAS は CPU のアトミック命令であり、いわゆるデータの不整合の問題は発生しません。

  • var5 は getInt Volatile ()を増やすことで得られるデータ変更の必要性を示す期待値で、メソッド名を見るとメインメモリの最新の値がわかります。
  • CompareAndSwapInt は、オペレーティング システムによって提供されるアトミック操作 CAS です。
    • 期待値 (取得したばかりの var5) がメイン メモリ内の値と同じである場合は、それを変更して true を返し、そうでない場合はループを終了します。
      • C言語のポインタと同様にメモリを直接操作して対応するデータを取得しますが、var1がこれ、var2がオフセットアドレスになります。
    • 期待値がメイン メモリ内の値と異なる場合は、false を返し、それを否定し、ループを継続して再試行します。

カスタムアトミックリファレンス

  • たとえば、AtomicInteger はアトミックな整数型です。他のアトミック型はありますか? たとえば、AtomicBook、AtomicOrder* などです。
  • できる!
  • ジェネリックに投げ込むClass AtomicReference<V>
@Getter
@ToString
@AllArgsConstructor
class User{
    String userName;
    int age;
}
public class CASDemo3 {
    public static void main(String[] args) {
        User zhangSan = new User("zhangsan", 20);
        User liSi = new User("lisi", 22);
        AtomicReference<User> userAtomicReference = new AtomicReference<>();
        userAtomicReference.set(zhangSan);
        System.out.println(userAtomicReference.compareAndSet(zhangSan,liSi)+"\t"+userAtomicReference.get().toString());
        System.out.println(userAtomicReference.compareAndSet(zhangSan,liSi)+"\t"+userAtomicReference.get().toString());
    }
}
true	User(userName=lisi, age=22)
false	User(userName=lisi, age=22)

CASとスピンロック

スピンロック

  • これは、ロックを取得しようとするスレッドがすぐにブロックされず、ループ内でロックの取得を試行することを意味します。

  • スレッドは、ロックが占有されていることを検出すると、ロックを取得するまでループを続けてロックの状態を判断します。この利点は、スレッド コンテキスト スイッチングの消費を削減できることですが、欠点は、ループが CPU を消費することです。

OpenJDK ソース コードで Unsafe.java を表示した場合

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
  • スピンのアイデアを体現しながらここに

  • true の場合は false を否定してループを終了し、false の場合は true を否定してループを継続します。

スピンロックを実装する

トピック: スピン ロックの実装
スピン ロックの利点: 待機のようなブロックを伴わない循環比較取得。

スピン ロックは CAS 操作によって完了します。スレッド A は最初に myLock メソッドを呼び出し、ロックを 5 秒間保持します。次にスレッド B が入ってきて、スレッドが現在ロックを保持していることがわかりますが、これは null ではないため、スレッドは待機することしかできません
。 A がロックを解除するまで回転させ、その後 B がロックを掴みました。

public class SpinLock {
    
    
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
    
    
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t"+"尝试获取锁");
        //只有没有线程占用的时候,才能加锁
        while (!atomicReference.compareAndSet(null,thread)){
    
    

         }
        //如果是空的,那么就吧thread放进去
        System.out.println(Thread.currentThread().getName()+"\t"+"获得了这个自旋锁");
    }
    public void unLock(){
    
    
        Thread thread = Thread.currentThread();
        //只有当前占用锁的线程才能进行解锁
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t"+"-------任务完成,解锁.....");
    }

    public static void main(String[] args) {
    
    
        SpinLock spinLock = new SpinLock();
        new Thread(()->{
    
    
            spinLock.lock();
            try {
    
     TimeUnit.SECONDS.sleep( 5 ); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
            spinLock.unLock();
        },"t1").start();
        new Thread(()->{
    
    
            spinLock.lock();
            spinLock.unLock();
        },"t2").start();
    }
}
t1	尝试获取锁
t1	获得了这个自旋锁
t2	尝试获取锁
t1	-------任务完成,解锁.....
t2	获得了这个自旋锁
t2	-------任务完成,解锁.....
  • while (!atomicReference.compareAndSet(null,thread))
    • CAS(V,A,B) と同等で、V はロックの現在の所有者、つまりロックを占有しているスレッドを表します。A(null) は、現在ロックを所有しているスレッドが存在しないという希望を表します。B は、実行中の現在のスレッドを表します。この方法。
      • 当ANULL の場合、現在のスピン ロックがどのスレッドにも所有されていないことを意味します。this.owner を変更してみてください。Thread.currrentThread()、ロックを保持しているオブジェクトを現在のスレッドに切り替えます

CASの欠点

  • ABAに関する質問

  • 長いサイクル時間と高いオーバーヘッド

  • 共有変数に対するアトミック操作のみが保証されます

長いサイクル時間と高いオーバーヘッド

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

do while 回転し続けると、CPU 時間が占有され続け、大量のオーバーヘッドが発生します。

  • 深刻なリソース競合 (深刻なスレッド競合) の場合、CAS スピンの確率が比較的高くなり、より多くの CPU リソースが浪費され、効率は同期時よりも低くなります。
    • CAS が失敗しても、試行を続けます。CAS が長期間失敗すると、CPU に多大なオーバーヘッドが発生する可能性があります。

ABA問題

CAS アルゴリズムの実装の重要な前提条件は、メモリ内の特定の時点のデータを取得し、現在の時点で比較および置換することであり、その場合、時間差によってデータが変化します。

たとえば、スレッド 1 がメモリ位置 V から A を取り出します。このとき、別のスレッド 2 もメモリから A を取り出し、スレッド 2 が値を B に変更する操作を実行し、その後スレッド 2 が変更します。 V の位置にあるデータが B に格納され、A になります。この時点で、スレッド 1 は CAS 操作を実行し、A がまだメモリ内にあることを確認し、スレッド 1 は正常に動作します。スレッド 1 の CAS 操作は成功しましたが、このプロセスに問題がないことを意味するわけではありません。
画像

ソリューション

  • ABA の質問にバージョン番号を導入する
    • CAS V==A の場合、バージョン番号は、現在のメイン メモリ V が他のスレッドによって繰り返し変更されているかどうかを判断するために使用されます。

JDKの実装

AtomicStampedReferenceバージョン番号 (以前のものとの違いに注意してくださいClass AtomicReference<V>)

AtomicStampedReference(V initialRef, int initialStamp)
创建一个新的 AtomicStampedReference与给定的初始值。

ABA問題再発

public class CASDemo4 {
    static AtomicReference atomicReference = new AtomicReference<>(100);
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(100, 101)+"\t"+atomicReference.get());
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(101, 100)+"\t"+atomicReference.get());
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+atomicReference.compareAndSet(100, 103)+"\t"+atomicReference.get());
        },"t2").start();
    }
}
t1操作true	101
t1操作true	100
t2操作true	103

バージョン番号を追加

public class CASDemo4 {
    static AtomicReference atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference(100,1);
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 首次版本号:"+stamp+"\t"+"数值为"+atomicStampedReference.getReference());//1-----------初始获得一样的版本号
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(100, 101,1,2)+"\t"
                    +"第二次操作版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(101, 100,2,3)+"\t"+
                    "第三次操作版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"操作"+"\t"+
                    atomicStampedReference.compareAndSet(100, 103,1,2)+"\t"+
                    "第四次版本号为"+atomicStampedReference.getStamp()+"\t"+
                    "数值为"+atomicStampedReference.getReference());
        },"t2").start();
    }
}
t1	 首次版本号:1	数值为100
t1操作	true	第二次操作版本号为2	数值为101
t1操作	true	第三次操作版本号为3	数值为100
t2操作	false	第四次操作版本号为3	数值为100

共有変数に対するアトミック操作のみが保証されます

  • 共有変数で操作を実行する場合、循環 CAS を使用してアトミック操作を保証できます。ただし、複数の共有変数で操作する場合、循環 CAS では操作のアトミック性を保証できません。この場合、ロックを使用できます。

JDK関連のアトミッククラスの使用

おすすめ

転載: blog.csdn.net/qq_50985215/article/details/131510738