Java併發編程之CAS二源碼追根溯源

Java併發編程之CAS二源碼追根溯源

在上一篇文章中,我們知道了什麼是CAS以及CAS的執行流程,在本篇文章中,我們將跟著源碼一步一步的查看CAS最底層實現原理。

本篇是《凱哥(凱哥Java:kagejava)併發編程學習》系列之《CAS系列》教程的第二篇:從源碼追根溯源查看CAS最底層是怎麼實現的。

本文主要內容:CAS追根溯源,徹底找到CAS的根在哪裡。

一:查看AtomicInteger.compareAndSet源碼

通過上一篇文章學習,我們知道了AtomicInteger.compareAndSet方法不加鎖可以保證原子性(其原理就是unsafe+cas實現的),我們來看看其源碼:

思考1:變量可見性

AtomicInteger對象(下文凱哥簡稱:atoInteger)怎麼保證變量內存可見性呢?

查看源碼:


Java併發編程之CAS二源碼追根溯源


思考2:為什麼上一篇13行的i.compareAndSet(1,1024)是false

Java併發編程之CAS二源碼追根溯源

發現創建對象的時候,初始值使用volatile修飾的。這樣就保證了變量的可見性

思考2:為什麼上一篇13行的i.compareAndSet(1,1024)是false

我們來看看atoInteger的compareAndSet方法。凱哥在上面添加了註釋。

Java併發編程之CAS二源碼追根溯源

在調用unsafe的compareAndSwapInt這個方法的時候,unsafe是什麼?this指的是什麼?valueOffset又是什麼呢?

我們接著查看atoInteger源碼:

Java併發編程之CAS二源碼追根溯源

我們發現Unsafe以及valueOffset都是從一個對象中獲取到的。

那麼this指的是什麼?其實this就是當前atoInteger對象。

那麼Unsafe對象在哪裡呢?

Java併發編程之CAS二源碼追根溯源

我們發現在sun.misc包下。這個包,我們學習Java得時候,好像沒見過呢。那麼它在哪裡呢?

Java併發編程之CAS二源碼追根溯源

原來在Jre得lib包下。如下圖:

Java併發編程之CAS二源碼追根溯源

我們想要看源碼,怎麼查看呢?發現不能看源碼啊。別急,這個文件的源碼可以從openJdk的源碼中查到。

接著,我們來查看OpenJdk8的源碼:

(PS:下載OpenJdk8源碼凱哥這裡就不贅述了。在文章最後,凱哥給出)

下載完,解壓之後,文件位置:openjdk\\jdk\\src\\share\\classes\\sun\\misc。如下圖:

Java併發編程之CAS二源碼追根溯源

我們來看看Unsafe類上面的註解:

Java併發編程之CAS二源碼追根溯源

A collection of methods for performing low-level, unsafe operations.

什麼意思呢?用於執行底層的(low-level,)、不安全操作的方法的集合。

就是說,這個類可以直接操作底層數據的。

需要說明的是:在這個對象中大量的方法使用了native來修飾(據網友統計高達82個)

Java併發編程之CAS二源碼追根溯源

我們知道,Java的方法使用native關鍵字修飾的,說明這個方法不是Java自身的方法(非Java方法),可能調用的是其他語言的。如C或C++語言的方法。

我們再來看看:unsafe.objectFieldOffse()中的

Java併發編程之CAS二源碼追根溯源

這個方法就是返回一個內存中訪問偏移量。

return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

Java併發編程之CAS二源碼追根溯源

在unsafe類中compareAndSwapInt方法也是native的。我們在來看看這個方法調用操作系統底層C++的代碼:

Java併發編程之CAS二源碼追根溯源

說明:

jint *addr:主內存中的變量值

old:對象工作區域的值

new_val:將要改變的值。

這三個是不是很熟悉,對。就是CAS的三個參數。

來看看在上一篇的demo代碼:

Java併發編程之CAS二源碼追根溯源

分析第13行為什麼返回false:

在11行的時候,設置主內存的變量值V=1.

在12行後,更新為V=2020了。

當執行到第13行的時候,

主內存:V=2020

程序工作區變量值jint *addr A的值:A=1

new_val:1024。

從調用C++代碼我們可以分析到:

Java併發編程之CAS二源碼追根溯源

在第5行的時候,因為1!=2020,所以return 的result就是false.

所以第13行輸出的是false.

思考3:atoInteger.getAndIncrement()是怎麼保證數據一致性的

我們接著跟源碼:

Java併發編程之CAS二源碼追根溯源

調用的是getAndAddInt方法。接著查看unsafe的源碼,就會發現CAS保證原子性的終極代碼。

CAS保證原子性終極方法,如下圖:

Java併發編程之CAS二源碼追根溯源

看看:getObjectVolatile。方法發現是native.如下圖:

Java併發編程之CAS二源碼追根溯源

再來看看compareAndSwapObject:

Java併發編程之CAS二源碼追根溯源

發現是native修飾的方法。說明不是Java的方法。這個我們等會再細說。

先來研究getAndSetObject:

Java併發編程之CAS二源碼追根溯源

源碼:

Java併發編程之CAS二源碼追根溯源

我們來模擬:atoInteger.getAndIncrement();

假設默認值是0. 主內存的值是0

在調用getAndSetObject方法的幾個參數說明:

Var1:當前atoInteger對象

Var2:當前偏移量(內存地址所在位置。如:三排四列)

Vart4:默認就是1

Var5:獲取到的主內存的值

Var5+var4:將要更新的值。

從源碼,我們看到是do while語句。為什麼不是while語句呢?因為先要獲取到主內存中變量最新的值,然後再判斷。所以選用了do while語句。

我們來看看當CPU1線程1和CPU2線程B來執行的時候:

Java併發編程之CAS二源碼追根溯源



兩個線程都從主內存copay了i的值到自己工作內存空間後,進行+1的操作。

假設線程1再執行+1操作後,準備往主內存回寫數據的時候,CPU1被掛起。然後CPU2競爭到資源之後,也操作i+1後,將更新後的值回寫到了主內存中。然後切換到CPU1了,CPU1接著執行。對比代碼分析:

Java併發編程之CAS二源碼追根溯源

線程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語句就是後面我們將要學到的自旋)


分享到:


相關文章: