1. 程式人生 > 程式設計 >簡單瞭解JavaCAS的相關知識原理

簡單瞭解JavaCAS的相關知識原理

JMM與問題引入

為啥先說JMM,因為CAS的實現類中維護的變數都被volatile修飾,這個volatile 是遵循JMM規範(不是百分百遵循,下文會說)實現的保證多執行緒併發訪問某個變數實現執行緒安全的手段

一連串的知識點慢慢縷

首先說什麼是JMM,JMM就是大家所說的java的記憶體模型,它是人們在邏輯上做出的劃分,或者可以將JMM當成是一種規範,有哪些規範呢? 如下

  • 可見性: 某一個執行緒對記憶體中的變數做出改動後,要求其他的執行緒在第一事件內馬上馬得到通知,在CAS的實現中,可見性其實是通過不斷的while迴圈讀取而得到的通知,而不是被動的得到通知
  • 原子性: 執行緒在執行某個操作的時,要麼一起成功,要麼就一起失敗
  • 有序性: 為了提高效能,編譯器處理器會進行指令的重排序,原始碼-> 編譯器優化重排 -> 處理器優化重排 -> 記憶體系統重排 -> 最終執行的命令

JVM執行的實體是執行緒,每一個執行緒在建立之後JVM都會為其建立一個工作空間,這個工作空間是每一個執行緒之間的私有空間,並且任何兩條執行緒之間的都不能直接訪問到對方的工作空間,執行緒之間的通訊,必須通過共享空間來中轉完成

JMM規定所有的變數全部存在主記憶體中,主記憶體是一塊共享空間,那麼如果某個執行緒相對主記憶體中共享變數做出修改怎麼辦呢? 像

下面這樣:

  • 將共享變數的副本拷貝到工作空間中
  • 對變數進行賦值修改
  • 將工作空間中的變數寫回到記憶體中

JMM還規定如下:

  • 任何執行緒在解鎖前必須將工作空間的共享變數立即重新整理進記憶體中
  • 執行緒在加鎖前必須讀取主記憶體中的值更新到自己的工作空間中
  • 加鎖和解鎖是同一把鎖

問題引入

這時候如果多個執行緒併發按照上面的三步走去訪問主記憶體中的共享變數的話就會出現執行緒安全性的問題,比如說 現在主記憶體中的共享變數是c=1,有AB兩個執行緒去併發訪問這個c變數,都想進行c++,現在A將c拷貝到自己的工作空間進行c++,於是c=2,於此同時執行緒B也進行c++,c在B的工作空間中=2,AB執行緒將結果寫回工作空間最終的結果就是2,而不是我們預期的3

相信怎麼解決大家都知道,就是使用JUC,中的原子類就能規避這個問題

而原子類的底層實現使用的就是CAS技術

什麼是CAS

CAS(compare and swap) 顧名思義: 比較和交換,在JUC中原子類的底層使用的都是CAS無鎖實現執行緒安全,是一門很炫的技術

如下面兩行程式碼,先比較再交換,即: 如果從主記憶體中讀取到的值為4就將它更新為2019

  AtomicInteger atomicInteger = new AtomicInteger(4);
  atomicInteger.compareAndSet(4,2019);

跟進AtomicInteger的原始碼如下,底層維護著一個int 型別的 變數,(當然是因為我選擇的原來類是AtomicInteger型別),並且這個int型別的值被 volatile 修飾

 private volatile int value;

 /**
  * Creates a new AtomicInteger with the given initial value.
  *
  * @param initialValue the initial value
  */
 public AtomicInteger(int initialValue) {
  value = initialValue;
 }

什麼是volatile

volatile是JVM提供的輕量的同步機制,為什麼是輕量界別呢?,剛才在上面說了JMM規範中提到了三條特性,而JVM提供的volatile僅僅滿足上面的規範中的 2/3,如下:

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排序

單獨的volatile是不能滿足原子性的,即如下程式碼在多執行緒併發訪問的情況下依然會出現執行緒安全性問題

private volatile int value;
 
public void add(){
 value++; 
}

那麼JUC的原子類是如何實現的 可以滿足原子性呢? 於是就不得不說本片博文的主角,CAS

CAS原始碼跟進

我們跟進AtomicInteger中的先遞增再獲取的方法 incrementAndGet()

 public final int incrementAndGet() {
  return unsafe.getAndAddInt(this,valueOffset,1) + 1;
 }

通過程式碼我們看到呼叫了Unsafe類來實現

什麼是Unsafe類?

進入Unsafe類,可以看到他裡面存在大量的 native方法,這些native方法全部是空方法,

這個unsafe類其實相當於一個後門,他是java去訪問呼叫系統上 C C++ 函式類庫的方法 如下圖

繼續跟進這個方法incrementAndGet() 於是我們就來到了我們的主角方法,關於這個方法倒是不難理解,主要是搞清楚方法中的var12345到底代表什麼就行,如下程式碼+註釋

var1: 上一個方法傳遞進來的: this,即當前物件
var2: 上一個方法傳遞進來的valueOffset,就是記憶體地址偏移量
  通過這個記憶體地址偏移量我能精確的找到要操作的變數在記憶體中的地址
  
var4: 上一個方法傳遞進來的1,就是每次增長的值
var5: 通過this和記憶體地址偏移量讀取出來的當前記憶體中的目標值
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;
 }

注意它用的是while迴圈,相對if(flag){} 這種寫法會多一次判斷,整體的思路就是 在進行修改之前先進行一次比較,如果讀取到的當前值和預期值是相同的,就自增,否則的話就繼續輪詢修改

小總結

通過上面的過程,其實就能總結出CAS的底層實現原理

  • volatile
  • 自旋鎖
  • unsafe類

補充: CAS通過Native方法的底層實現,本質上是作業系統層面上的CPU的併發原語,JVM會直接實現出彙編層面的指令,依賴於硬體去實現,此外,對於CPU的原語來說,有兩條特性1,必定連續,2.不被中斷

CAS的優缺點

優點:

它的底層我們看到了通過do-while 實現的自旋鎖來實現,就省去了在多個執行緒之間進行切換所帶來的額外的上下文切換的開銷

缺點:

  • 通過while迴圈不斷的嘗試獲取,省去了上下文切換的開銷,但是佔用cpu的資源
  • CAS只能保證一個共享變數的原子性,如果存在多個共享變數的話不得不加鎖實現
  • 存在ABA問題

ABA問題

什麼是ABA問題

我們這樣玩,還是AB兩個執行緒,給AtomicInteger賦初始值0

A執行緒中的程式碼如下:

  Thread.sleep(3000);
  atomicInteger.compareAndSet(0,2019);

B執行緒中的程式碼如下:

  atomicInteger.compareAndSet(0,1);
  atomicInteger.compareAndSet(1,0);

AB執行緒同時啟動,雖然最終的結果A執行緒能成果的將值修改成2019,但是它不能感知到在他睡眠過程中B執行緒對資料進行過改變,換句話說就是A執行緒被B執行緒欺騙了

ABA問題的解決--- AtomicStampedRefernce.java

帶時間戳的原子引用,實現的機制就是通過 原子引用+版本號來完成,每次對指定值的修改相應的版本號會加1,例項如下

  // 0表示初始化,1表示初始版本號
  AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(0,1);
  reference.getStamp(); // 獲取版本號
  reference.attemptStamp(1,2); // 期待是1,如果是1就更新為2

原子引用

JUC中我們可以找到像AtomicInteger這樣已經定義好了實現類,但是JUC沒有給我們提供類似這樣 AtomicUser或者 AtomicProduct 這樣自定義型別的原子引用型別啊,不過java仍然是提供了後門就是 原子引用型別

使用例項:

  User user = getUserById(1);
  AtomicReference<User> userAtomicReference = new AtomicReference<User>();
  user.setUsername("張三");
  userAtomicReference.compareAndSet(user,user);

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。