CAS底層原理
- 什么是CAS
- CAS底層原理
- Unsafe
- CAS
- CAS的缺點
- ABA問題
- 原子類參考
- ABA問題解決
- 總結
什么是CAS
CAS 的英文是compare and set,也就是比較并交換,首先介紹一下比較重要的三個概念:initialValue(初始值),expect(期望值),update(更新值),
初始值就是變數最初的值
期望值就是執行緒在操作變數之前鎖期望的值
更新值就是執行緒要將變數修改成什么值
為了便于理解,首先大概介紹一下JAVA的記憶體模型,也可以看一下我之前寫的一篇文章:Java關鍵字volatile全面決議和實體講解.
JVM運行程式的物體是執行緒,而每個執行緒創建時由JVM分配各自的作業記憶體,執行緒的作業記憶體是各自私有的資料區域,互相各不影響, java記憶體模型中所有的變數是存盤在主記憶體中的,主記憶體是執行緒共享的區域,但是執行緒對變數的操作(讀取賦值)必須在作業記憶體中進行, 當多個執行緒訪問同一個變數(放在主存中)的時候,并不是直接在主存中操作變數,而是將此物件分別拷貝到每一個執行緒各自的作業記憶體中, 當其中一個執行緒操作了變數之后,需要將修改后的物件(也就是最新值)重新寫回主記憶體,也要將最新值同步給其他的執行緒,(可見性:讓其他的執行緒可以看到,) 執行緒之間的通信(傳值)必須通過主記憶體來完成,

下面來看一個場景 :
場景:假設有一個變數num,此時它的值是1(初始值),現在執行緒T1將要操作變數num更新值為2021(更新值),但是T1不能直接去更新num,因為num的值可能被其他執行緒修改成了其它的值,所以現在T1執行緒需要先比較一下num的值,T1期望num的值是1(期望值),首先T1去主存中讀取num的值,如果讀取的值是1,與T1執行緒的期望值相同,則更新num的值是2021,如果在T1操作之前其他執行緒將num的值更新為2020,那么T1的期望值(1)與num現在的值(2020)不相等,則更新失敗,
場景對應的代碼是:
AtomicInteger num = new AtomicInteger(1);
// boolean flag1 = num.compareAndSet(1, 2020);
boolean flag2 = num.compareAndSet(1, 2021);
// System.out.println(flag1); // 注釋的兩行,模擬另外執行緒更新num,回傳true
System.out.println(flag2); // 解除注釋,回傳false,因為上邊的代碼已經將num的值更新了,將兩行注釋,回傳true,說明更新成功
當然此處只是模擬了多執行緒,其實上邊的代碼只有一個執行緒操作,但是也并不影響對于CAS的理解,
CAS底層原理
java記憶體模型中需要遵循原子操作,原子操作就是一個或多個操作為一個整體,要么都執行且不會受到任何因素的干擾而中斷,要么都不執行,
而我們常用的1++并不是原子操作,而是分為了三步:1、讀記憶體到暫存器;2、在暫存器中自增;3、寫回記憶體,這樣就會導致多執行緒訪問的時候導致執行緒不安全的問題,
我們可以通過命令javap -c進行反匯編,我們可以看到i++操作其實是三條指令:
0: iconst_1
1: istore_1
2: iinc
那么如果要保證執行緒安全就要保證原子性,java中有一個家族:原子類,在java.util.concurrent.atomic包下,其實CAS的底層就是保證了原子性,那么他是如何保證原子性的呢?兩個重要的點:UnSafe類和CAS思想(自旋),我們以AtomicInteger這個原子類進行研究,
Unsafe
AtomicInteger num = new AtomicInteger(1);
num.getAndIncrement();
上邊的這行代碼是AtomicInteger + 1 的操作,下邊來看一下getAndIncrement方法做了什么,
/**
* 以原子方式將當前值遞增 1
*
* this: 當前實體物件
* valueOffset: 記憶體偏移量,可以簡單的理解為記憶體地址
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
查看AtomicInteger原始碼可知其中一個很重要的類就是Unsafe類,也是CAS原理的核心之一,還有一個volatile型別的變數,就是讓所有的執行緒可見,
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 獲取物件的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// volatile修飾,變數所有執行緒可見
private volatile int value;
-
我們查看Unsafe類可以發現里邊的方法都是native修飾的,Unsafe是CAS的核心類,由于java方法無法直接訪問底層系統,需要通過本地方法(native)方法訪問,Unsafe相當于一個后門,基于該類可以直接操作特定記憶體的資料,Unsafe類存在于sun.misc包中,其內部方法操作可以像C的指標一樣直接操作記憶體(通過記憶體偏移量定位),因為java中的CAS操作的執行依賴于Unsafe類的方法,
Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的所有的方法都直接呼叫作業系統底層資源執行相應的任務,
-
變數valueOffset表示該變數值在記憶體中的偏移地址,因為Unsafe就是根據記憶體偏移地址獲取資料的,
-
volatile修飾,變數所有執行緒可見.
下面再來看一下unsafe.getAndAddInt方法,
/**
*
* unsafe.getAndAddInt(this, valueOffset, 1)
* 引數對應 var1 var2 var4
*
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 根據this和記憶體偏移量獲取物件,相當于將主存中的變數拷貝到執行緒自己的作業記憶體,表示拿變數的最新值,
var5 = this.getIntVolatile(var1, var2);
// this.compareAndSwapInt(var1, var2, var5, var5 + var4)
// 進行比較交換:var1,var2用于確定初始值,var5表示期望值,初始值和期望值進行比較,相等
// 則進行加操作(var5 + var4)得到更新值,并回傳true,不相等回傳false,
// while的回圈條件中取反表示如果初始值和期望值相等(!true),可以進行更新,并跳出回圈,
// 如果初始值和期望值不相等(!false),不可以進行更新,一直回圈直到相等,
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS
CAS是一條CPU并發原語,它的功能是判斷記憶體某個位置是否為預期值,如果是則改為新的值,這個程序是原子的,
CAS并發原語體現在JAVA語言匯總就是Unsafe類中的各個方法,呼叫Unsafe類中的CAS方法,JVM會幫我們實作CAS匯編指令,這是一種完全依賴于硬體的功能,通過它實作了原子操作,由于CAS是一種系統原語,原語屬于作業系統用語范疇,是由若干條指令組成的,用于完成某個功能的一個程序,并且原語的執行必須是連續的,在執行程序中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的資料不一致性,
CAS的缺點
- Unsafe類中的getAndAddInt方法有一個while回圈,用于比較初始值和期望值,假如有很多的執行緒(10w個)同時訪問變數,變數的值被修改了很多次,每一個執行緒都需要從主存讀取交換,很容易導致CAS失敗,就會一直嘗試,如果CAS長時間一直不成功,就會給CPU帶來很大的開銷,
- 從原始碼中(unsafe.getAndAddInt(this, valueOffset, 1))可以看出來,this表示的當前物件,也就是說CAS只能保證一個共享變數的原子操作,對于多個共享變數需要加鎖保證原子性,
- ABA問題,
ABA問題
定義:CAS演算法實作一個重要前提是需要取出記憶體中某時刻的資料并在當下時刻比較并替換,那么在這個時間差類會導致資料的變化,

如上圖,T1執行緒要將變數num更新成2021,T2執行緒要將變數num更新成2,
首先T1和T2分別將主存中的num讀取到自己的作業記憶體,但是不巧的是T2此時睡眠一秒,暫停了操作變數,此時T1執行緒搶占了CPU資源,將num更新成了2021并寫回主存,但是T1后來又不想更新,后悔了,想皮一下,就又改回原來的1,此時的T2睡醒了,查看此時主存的num還是1就將num的值變成了2并寫回主存,但是T2并不知道T1已經將num的值更新了兩次,甚至更多次,這就是ABA的問題了,
所以說盡管T2的CAS的操作成功,但是并不代表這個程序是沒有問題的,
看一下上邊程序的代碼:
public class Test {
static AtomicInteger num = new AtomicInteger(1);
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "開始啟動");
try {
// 讓執行緒等待1秒
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
boolean flag = num.compareAndSet(1,2);
System.out.println(Thread.currentThread().getName() + "已經結束," + "\t更新是否成功" + flag + "\tnum >>> " + num.get());
},"T1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "開始啟動");
boolean flag = false;
flag = num.compareAndSet(1,2021);
System.out.println(Thread.currentThread().getName() + "執行," + "\t更新是否成功" + flag + "\tnum >>> " + num.get());
flag = num.compareAndSet(2021,1);
System.out.println(Thread.currentThread().getName() + "執行," + "\t更新是否成功" + flag + "\tnum >>> " + num.get());
},"T2").start();
}
}
輸出結果為:
T1開始啟動
T2開始啟動
T2執行, 更新是否成功true num >>> 2021
T2執行, 更新是否成功true num >>> 1
T1已經結束, 更新是否成功true num >>> 2
如果理解不了的話,那你們應該都看過周星馳的電影情圣吧,烏鴉哥變豬頭的那一段就是ABA問題了,感興趣的可以看一下,
原子類參考
在java中juc包給我們提供了很多的原子類

但是這還是滿足不了我們的需求,假如我們的自建的一個類User,怎么保證原子性呢?我們可以使用AtomicReference類,AtomicReference帶的泛型就是我們要保證原子性的類了,
public class AtomTest {
public static void main(String[] args) {
User user1 = new User(11,"zhangsan");
User user2 = new User(12,"lisi");
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1, user2) + "\t" + atomicReference.get()); // true User{age=12, name='lisi'}
System.out.println(atomicReference.compareAndSet(user1, user2) + "\t" + atomicReference.get()); // false User{age=12, name='lisi'}
}
}
class User {
private int age;
private String name;
public User(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "User{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
上邊的代碼就是AtomicReference基本使用的demo,從輸出結果可以看出來,對于我們的自己創建的類也可以保證原子性,
ABA問題解決
ABA問題的解決可以添加一種機制,就是版本號,類似于時間戳,就是說變數每新增一次,版本號就 加1 ,當執行緒在操作變數的時候,不僅僅要比較期望值,還要比較版本號,如果期望值相同,但是版本號不同則也不允許修改,
以上邊的例子來說,num的值變化為1 >> 2021 >> 1,那么添加版本號之后就是1(1)>> 2021(2) >> 1(3)(括號里邊的數字為版本號),當T2訪問變數的時候,T2此時的變數為1(1),1(1)和1(3)相比較期望值相同,但是版本號不同,所以T2無法更新num的值,
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class Test {
static AtomicInteger num = new AtomicInteger(1);
// initialRef 初始值 initialStamp 初始版本號
static AtomicStampedReference<Integer> stampedNum = new AtomicStampedReference<>(1, 1);
public static void main(String[] args) {
System.out.println("==========================ABA問題的產生==================================");
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "開始啟動");
try {
// 讓執行緒等待1秒
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
boolean flag = num.compareAndSet(1,2);
System.out.println(Thread.currentThread().getName() + "已經結束," + "\t更新是否成功" + flag + "\tnum >>> " + num.get());
},"T1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "開始啟動");
boolean flag = false;
flag = num.compareAndSet(1,2021);
System.out.println(Thread.currentThread().getName() + "執行," + "\t更新是否成功" + flag + "\tnum >>> " + num.get());
flag = num.compareAndSet(2021,1);
System.out.println(Thread.currentThread().getName() + "執行," + "\t更新是否成功" + flag + "\tnum >>> " + num.get());
},"T2").start();
try {
// 讓執行緒等待2秒,保證上邊的ABA問題已經發生一次
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("==========================ABA問題的解決==================================");
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "開始啟動");
try {
// 讓執行緒等待1秒,保證T3拿到版本號之后,T4拿到同一個版本號
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
int stamp = stampedNum.getStamp();
System.out.println(stamp);
System.out.println(Thread.currentThread().getName() + "\t第一次版本號是 >>> " + stampedNum.getStamp());
boolean flag = false;
flag = stampedNum.compareAndSet(1, 3, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t是否修改成功 >>> " + flag + "\t第二次版本號是 >>> " + stampedNum.getStamp() + "\tstampedNum >>> " + stampedNum.getReference());
stamp = stampedNum.getStamp();
System.out.println(stamp);
flag = stampedNum.compareAndSet(3, 1, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t是否修改成功 >>> " + flag + "\t第三次版本號是 >>> " + stampedNum.getStamp() + "\tstampedNum >>> " + stampedNum.getReference());
},"T3").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "開始啟動");
int stamp = stampedNum.getStamp();
try {
// 讓執行緒等待3秒,第一秒保證T4和T3執行緒拿到同一個版本號
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t第一次版本號是 >>> " + stamp);
boolean flag = false;
flag = stampedNum.compareAndSet(1, 2, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t是否修改成功 >>> " + flag + "\t第二次版本號是 >>> " + stampedNum.getStamp() + "\tstampedNum >>> " + stampedNum.getReference());
},"T4").start();
}
}
輸出結果為:
==========================ABA問題的產生==================================
T1開始啟動
T2開始啟動
T2執行, 更新是否成功true num >>> 2021
T2執行, 更新是否成功true num >>> 1
T1已經結束, 更新是否成功true num >>> 2
==========================ABA問題的解決==================================
T3開始啟動
T4開始啟動
1
T3 第一次版本號是 >>> 1
T3 是否修改成功 >>> true 第二次版本號是 >>> 2 stampedNum >>> 3
2
T3 是否修改成功 >>> true 第三次版本號是 >>> 3 stampedNum >>> 1
T4 第一次版本號是 >>> 1
T4 是否修改成功 >>> false 第二次版本號是 >>> 3 stampedNum >>> 1
總結
1.CAS中文就是比較并交換,三個重要的概念:初始值,期望值,更新值, 2.CAS保證資料的原子性,它的核心是UnSafe類和自旋,重繪主存中的資料,使每個執行緒都拿到最新的變數值, 3. CAS有三個問題: a. 由于存在while回圈,當很多執行緒同時訪問的時候就會給CPU帶來很大的開銷; b.CAS只能保證一個共享變數的原子性; c.ABA問題 4. 通過使用帶有版本號的原子參考類(AtomicStampedReference)來解決ABA問題轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/292872.html
標籤:其他
上一篇:xss之CSP bypass
下一篇:SQLmap常用命令/使用教程
