1. 程式人生 > 其它 >2.CAS原理

2.CAS原理

技術標籤:JUC

CAS原理

  • CAS Compare-And-Swap
  • 判斷記憶體中的某一個位置的值是否為預期值,如果是則修改為新的值,過程是原子性的
  • CAS併發原語體現在sun.misc.Unsafe類的各個方法中.呼叫Unsafe類中的CAS方法,JVM會實現CAS的彙編指令,這是一種完全依賴硬體的功能,通過它實現了原子操作,由於CAS屬於系統原語,原語屬於作業系統應用範疇,是由若干指令組成,用於完成某一個特定功能,並且原語執行必須是連續的,在執行過程中不允許被中斷,CAS是一條CPU的原子指令,不會存線上程安全問題

程式碼展示

public class CASDemo1 {

    public
static void main(String[] args) { // 初始化AtomicInteger AtomicInteger atomicInteger = new AtomicInteger(1); // 判斷原值是1 修改成功為100 atomicInteger.compareAndSet(1,100); System.out.println(atomicInteger.get()); // 原值已經被修改為100了不是期望值1 此次修改失敗 atomicInteger.compareAndSet
(1,200); System.out.println(atomicInteger.get()); } }

CAS底層原理

  • Unsafe類
  • getAndIncrement使用自旋鎖的思想
private static final Unsafe unsafe = Unsafe.getUnsafe(); 
public final int getAndIncrement() {
    // this表示當前atomicInteger物件
    // valueOffset 記憶體偏移量
    // 被加數
        return unsafe.getAndAddInt(this
, valueOffset, 1); }
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 配合 volatile 其他執行緒修改之後立刻通知其他執行緒
            
            var5 = this.getIntVolatile(var1, var2);
            // 如果此時except的值已經跟真實的值不一樣的 while返回false 取反
            // 再進行一次操作 直到更新成功
            // var1 this  var2 記憶體變異量 var5 期望值 var5+var4相加值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
  • Unsafe是CAS的核心類,由於java方法無法直接訪問底層作業系統,需要通過本地方法來訪問,Unsafe是一個後門,基於Unsafe可以直接操作特定的記憶體資料.Unsafe在rt.jar中的sun.misc包中,內部方法操作可以像C指標一樣直接操作記憶體,java中的CAS操作都是依賴Unsafe類
  • 以AtomicInteger 中的 getAndIncrement為例
    • 底層呼叫Unsafe的getAndAddInt方法
    • 取出記憶體中的值與執行緒中的值進行比較如果相同就進行+1操作,如果不同自旋再進行一次
  • 此處沒有使用synchronized修飾,使用CAS,提高了併發性,也能保證一致性.每次執行緒進行都是進行一個do while 迴圈,不斷的獲取記憶體的值,判斷是不是最新值,如果不是再重新獲取,如果是則進行更新操作

假設執行緒A 和 執行緒B同時進行getAndIncrement操作

  1. 如AtomicInteger的value 初始值為5,此時主記憶體中AtomicInteger的value是5,根據java記憶體模型,AB執行緒各自拷貝主記憶體中的值5,到各自的工作記憶體中
  2. 此時執行緒A 通過getIntVolatile方法獲取到最新的value是5,但是此時執行緒A被掛起
  3. 執行緒B進入通過getIntVolatile也拿到的最新value的值是5,然後繼續執行,將值改成6,完成了getAndIncrement操作 執行緒B終止
  4. 此時執行緒A被喚醒,進去while迴圈的CAS操作,方法此時拿到的value5 跟主記憶體地址的value=6已經不一致,此次更新失敗返回false,while迴圈中取反再次進入getIntVolatile方法獲取最新值
  5. 以為value被volatile修飾,執行緒B修改對其他執行緒可見,執行緒A再次獲取value的值是6為最新值,再次呼叫compareAndSwapInt此次成功

底層彙編原理

  • Unsafe類中的compareAndSwapInt為本地方法,由本地方法unsafe.cpp中實現
  • 使用了CPU的原語,CPU原語使用了多條彙編指令組成是不可分割的單位,也不會存線上程安全問題
  • 本質上去拿到變數value的記憶體地址獲取到真實的最新值
  • 通過彙編執行Atomic::cmpxchg實現比較替換,其中引數X是即將更新的值,引數e是原記憶體的值

CAS缺點

  • 迴圈時間長,開銷大 do while迴圈,沒如果長時間比較不成功一直在迴圈,最差的情況,就是某一個執行緒取到的值和預期值都不一樣
  • 只能保證一個共享變數的原子操作,但多個共享變數需要保證原子操作,只能使用鎖來保證原子性
  • ABA問題

ABA問題

  • 假設有T1 T2 兩個執行緒,T1的執行時間為10秒,T2的執行時間為2秒

  • 最開始T1 T2 從主存中獲取資料num的最新值為5

  • 此時T2因為執行的速度更快將num 從5 更新成了100, 然後又將100 重新改為了5,此時T2執行緒直接結束

  • T1執行緒10秒之後從記憶體中讀取num為5,更預期值一樣,認為沒有被人更改過,直接成功,其實num的值是已經被其他執行緒從5改成100再改回5了

  • ABA出現的問題的本質在於,CAS演算法實現的重要前提是需要從記憶體種的某一個時刻取出資料,並在進行比較和替換,那段時間的空閒,可能會導致資料發生了變化

  • CAS只管開頭和結尾,只要頭和尾是一樣的那麼就修改成功,中間過程可能會被其他執行緒所修改

ABA解決方式

  • AtomicStampedReference類 帶stamp的原子引用型別

  • /**
    expectedReference: 期望引用
    newReference: 更新的新引用
    expectedStamp: 期望的stamp
    newStamp: 新stamp
    */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
        }
    
  • 實際使用 AtomicStampedReference

        private static void atomicStampRefDemo() {
            AtomicStampedReference<Integer> num = new AtomicStampedReference<>(5, 0);
            new Thread("T1") {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println(" t1執行緒第一次更新: " + num.compareAndSet(5, 100, num.getStamp(), num.getStamp() + 1) + " value = " + num.getReference() + " stamp= " + num.getStamp());
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(" t1執行緒第二次更新: " + num.compareAndSet(100, 5, num.getStamp(), num.getStamp() + 1) + " value = " + num.getReference() + " stamp= " + num.getStamp());
                }
            }.start();
            new Thread("T2") {
                @SneakyThrows
                @Override
                public void run() {
                    int stamp = num.getStamp();
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println("t2 執行緒嘗試更新的stamp " + stamp);
                    System.out.println(" t2執行緒嘗試更新: " + num.compareAndSet(5, 200, stamp, stamp + 1));
                }
            }.start();
    
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
            System.out.println("最終num的值為: " + num.getReference());
        }
    

CAS所有例項程式碼

package com.corn.juc.cas;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author : Jim Wu
 * @version 1.0
 * @function :
 * @since : 2020/12/18 11:08
 */
@Data
@AllArgsConstructor
class User {
    private String name;

    private int age;
}

public class CASDemo1 {

    public static void main(String[] args) {
        // 基本的Atomic CAS應用
//        baseCASdemo();
        // atomicReference
//        atomicRefDemo();
        // atomic Stamp reference
//        ABAProblem();
        // 使用 AtomicStampReference解決ABA問題
        atomicStampRefDemo();

    }

    /**
     * 最基本的 Atomic CAS應用
     */
    private static void baseCASdemo() {
        // 初始化AtomicInteger
        AtomicInteger atomicInteger = new AtomicInteger(1);
        // 判斷原值是1 修改成功為100
        atomicInteger.compareAndSet(1, 100);
        System.out.println(atomicInteger.get());
        // 原值已經被修改為100了不是期望值1 此次修改失敗
        atomicInteger.compareAndSet(1, 200);
        System.out.println(atomicInteger.get());
    }

    /**
     * 原子引用類使用
     */
    private static void atomicRefDemo() {
        User u1 = new User("jack", 15);
        User u2 = new User("lily", 25);
        User u3 = new User("tom", 45);
        AtomicReference<User> userAtomicReference = new AtomicReference<>(u1);

        System.out.println(userAtomicReference.compareAndSet(u1, u2) + " current user -> " + userAtomicReference.get());
        System.out.println(userAtomicReference.compareAndSet(u1, u3) + " current user -> " + userAtomicReference.get());
    }

    /**
     * ABA 問題演示
     */
    private static void ABAProblem() {
        // 演示ABA問題
        AtomicReference<Integer> num = new AtomicReference<>(5);

        new Thread("t1") {
            @SneakyThrows
            @Override
            public void run() {
                num.compareAndSet(5, 100);
                TimeUnit.SECONDS.sleep(1);
                num.compareAndSet(100, 5);
            }
        }.start();

        new Thread("t2") {
            @SneakyThrows
            @Override
            public void run() {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(num.compareAndSet(5, 200) + " ABA 問題體現 " + num.get());
            }
        }.start();

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("t1 執行緒執行更快已經將num 從5->100->5 ,t2喚醒之後發現expect=5 " + num.get());
    }

    /**
     * 使用 AtomicStampedReference 通過新增stamp的方法解決ABA問題
     */
    private static void atomicStampRefDemo() {
        AtomicStampedReference<Integer> num = new AtomicStampedReference<>(5, 0);
        new Thread("T1") {
            @SneakyThrows
            @Override
            public void run() {
                System.out.println(" t1執行緒第一次更新: " + num.compareAndSet(5, 100, num.getStamp(), num.getStamp() + 1) + " value = " + num.getReference() + " stamp= " + num.getStamp());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(" t1執行緒第二次更新: " + num.compareAndSet(100, 5, num.getStamp(), num.getStamp() + 1) + " value = " + num.getReference() + " stamp= " + num.getStamp());
            }
        }.start();
        new Thread("T2") {
            @SneakyThrows
            @Override
            public void run() {
                int stamp = num.getStamp();
                TimeUnit.SECONDS.sleep(2);
                System.out.println("t2 執行緒嘗試更新的stamp " + stamp);
                System.out.println(" t2執行緒嘗試更新: " + num.compareAndSet(5, 200, stamp, stamp + 1));
            }
        }.start();

        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最終num的值為: " + num.getReference());
    }
}