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);
歡迎關注我, 會繼續更新筆記
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】
※台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"
※網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整