Java併發編程之CAS二源碼追根溯源
在上一篇文章中,我們知道了什麼是CAS以及CAS的執行流程,在本篇文章中,我們將跟著源碼一步一步的查看CAS最底層實現原理。
本篇是《凱哥(凱哥Java:kagejava)併發編程學習》系列之《CAS系列》教程的第二篇:從源碼追根溯源查看CAS最底層是怎麼實現的。
本文主要內容:CAS追根溯源,徹底找到CAS的根在哪裡。
一:查看AtomicInteger.compareAndSet源碼
通過上一篇文章學習,我們知道了AtomicInteger.compareAndSet方法不加鎖可以保證原子性(其原理就是unsafe+cas實現的),我們來看看其源碼:
思考1:變量可見性
AtomicInteger對象(下文凱哥簡稱:atoInteger)怎麼保證變量內存可見性呢?
查看源碼:
思考2:為什麼上一篇13行的i.compareAndSet(1,1024)是false
發現創建對象的時候,初始值使用volatile修飾的。這樣就保證了變量的可見性
思考2:為什麼上一篇13行的i.compareAndSet(1,1024)是false
我們來看看atoInteger的compareAndSet方法。凱哥在上面添加了註釋。
在調用unsafe的compareAndSwapInt這個方法的時候,unsafe是什麼?this指的是什麼?valueOffset又是什麼呢?
我們接著查看atoInteger源碼:
我們發現Unsafe以及valueOffset都是從一個對象中獲取到的。
那麼this指的是什麼?其實this就是當前atoInteger對象。
那麼Unsafe對象在哪裡呢?
我們發現在sun.misc包下。這個包,我們學習Java得時候,好像沒見過呢。那麼它在哪裡呢?
原來在Jre得lib包下。如下圖:
我們想要看源碼,怎麼查看呢?發現不能看源碼啊。別急,這個文件的源碼可以從openJdk的源碼中查到。
接著,我們來查看OpenJdk8的源碼:
(PS:下載OpenJdk8源碼凱哥這裡就不贅述了。在文章最後,凱哥給出)
下載完,解壓之後,文件位置:openjdk\\jdk\\src\\share\\classes\\sun\\misc。如下圖:
我們來看看Unsafe類上面的註解:
A collection of methods for performing low-level, unsafe operations.
什麼意思呢?用於執行底層的(low-level,)、不安全操作的方法的集合。
就是說,這個類可以直接操作底層數據的。
需要說明的是:在這個對象中大量的方法使用了native來修飾(據網友統計高達82個)
我們知道,Java的方法使用native關鍵字修飾的,說明這個方法不是Java自身的方法(非Java方法),可能調用的是其他語言的。如C或C++語言的方法。
我們再來看看:unsafe.objectFieldOffse()中的
這個方法就是返回一個內存中訪問偏移量。
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
在unsafe類中compareAndSwapInt方法也是native的。我們在來看看這個方法調用操作系統底層C++的代碼:
說明:
jint *addr:主內存中的變量值
old:對象工作區域的值
new_val:將要改變的值。
這三個是不是很熟悉,對。就是CAS的三個參數。
來看看在上一篇的demo代碼:
分析第13行為什麼返回false:
在11行的時候,設置主內存的變量值V=1.
在12行後,更新為V=2020了。
當執行到第13行的時候,
主內存:V=2020
程序工作區變量值jint *addr A的值:A=1
new_val:1024。
從調用C++代碼我們可以分析到:
在第5行的時候,因為1!=2020,所以return 的result就是false.
所以第13行輸出的是false.
思考3:atoInteger.getAndIncrement()是怎麼保證數據一致性的
我們接著跟源碼:
調用的是getAndAddInt方法。接著查看unsafe的源碼,就會發現CAS保證原子性的終極代碼。
CAS保證原子性終極方法,如下圖:
看看:getObjectVolatile。方法發現是native.如下圖:
再來看看compareAndSwapObject:
發現是native修飾的方法。說明不是Java的方法。這個我們等會再細說。
先來研究getAndSetObject:
源碼:
我們來模擬:atoInteger.getAndIncrement();
假設默認值是0. 主內存的值是0
在調用getAndSetObject方法的幾個參數說明:
Var1:當前atoInteger對象
Var2:當前偏移量(內存地址所在位置。如:三排四列)
Vart4:默認就是1
Var5:獲取到的主內存的值
Var5+var4:將要更新的值。
從源碼,我們看到是do while語句。為什麼不是while語句呢?因為先要獲取到主內存中變量最新的值,然後再判斷。所以選用了do while語句。
我們來看看當CPU1線程1和CPU2線程B來執行的時候:
兩個線程都從主內存copay了i的值到自己工作內存空間後,進行+1的操作。
假設線程1再執行+1操作後,準備往主內存回寫數據的時候,CPU1被掛起。然後CPU2競爭到資源之後,也操作i+1後,將更新後的值回寫到了主內存中。然後切換到CPU1了,CPU1接著執行。對比代碼分析:
線程1在執行do後得到的值var5=1而不是0
然後while裡面執行:var1和var2運算後的結果是0(工作區的值)。
因為0!=5 .所以this.comparAndSwapInt的值是false.
又因為前面有個! 非得符號。也就是!false。我們知道!false就是true.
也就是while(true)。While(true)後,接著循環執行。線程會放棄原有操作,重新從主內存中獲取到最新數據(此時就是1了),然後再進行操作後。
又到了do,獲取在主內存最新數據是1.接著走while()
因為,var1,var2獲取到工作區的值是1 var5也等於1.1=1,成立了,執行var5+var5=1+1=2,來更新主內存的數據後返回true.
又因為前面有個!非的符號。所以就是while(!true),也就是while(false)。退出循環,返回var5的值。
結論:
通過上面的運行分析,我們發現atoInteger的getAndIncrement方法保證原子性是unsafe+CAS來保證變量原子性的(其中do while語句就是後面我們將要學到的自旋)
閱讀更多 凱哥java 的文章