2.CAS原理
阿新 • • 發佈:2021-01-06
技術標籤: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操作
- 如AtomicInteger的value 初始值為5,此時主記憶體中AtomicInteger的value是5,根據java記憶體模型,AB執行緒各自拷貝主記憶體中的值5,到各自的工作記憶體中
- 此時執行緒A 通過getIntVolatile方法獲取到最新的value是5,但是此時執行緒A被掛起
- 執行緒B進入通過getIntVolatile也拿到的最新value的值是5,然後繼續執行,將值改成6,完成了getAndIncrement操作 執行緒B終止
- 此時執行緒A被喚醒,進去while迴圈的CAS操作,方法此時拿到的value5 跟主記憶體地址的value=6已經不一致,此次更新失敗返回false,while迴圈中取反再次進入getIntVolatile方法獲取最新值
- 以為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());
}
}