從同步原語看非阻塞同步以及Java中的應用

風沙迷了眼發表於2019-06-15

  非阻塞同步:基於衝突檢測的樂觀併發策略,通俗講就是先進行操作,如果沒有其他執行緒爭用共享資料,那操作就成功了,如果爭用資料有衝突那就採用其他的補償措施(最常見的就是不斷重試直到成功),這種樂觀的併發策略使得很多執行緒不需要因為競爭失敗直接掛起,這種同步措施稱為非阻塞同步。下面我們就從硬體原語開始瞭解非阻塞同步,並看一看在Java中非阻塞同步的一些應用。

一、從硬體原語上理解同步(非特指Java)

  同步機制是多處理機系統的重要組成部分,其實現方式除了關係到計算的正確性之外還有效率的問題。同步機制的實現通常是在硬體提供的同步指令的基礎上,在通過使用者級別軟體例程實現的。上面說到的樂觀策略實際上就是建立在硬體指令集的基礎上的(我們需要實際操作和衝突檢測是原子性的),一般有下面的常用指令:測試並設定(test_and_set)、獲取並增加(fetch_and_increment)、原子交換(Atomic_Exchange)、比較並交換(CAS)、載入連線條件儲存(LL/SC),下面我們會講到這些以及通過這些硬體同步原語實現的旋轉鎖和柵欄同步。

1、基本硬體原語

  在多處理機中實現同步,所需的主要功能是一組能以原子操作讀出並修改儲存單元的硬體原語。如果沒有這種操作,建立基本的同步原語的代價會非常大。基本硬體原語有幾種形式提供選擇,他們都能以原子操作的方式讀改儲存單元,並指出進行的操作是否能以原子形式進行,這些原語作為基本構建提供構造各種各樣的使用者及同步操作。

  一個典型的例子就是原子交換(Atomic Exchange),他的功能是將一個儲存單元中的值和一個暫存器的值進行交換。我們看看這個原語怎樣構造一個我們通常意義上說的簡單的鎖。

  假設現在我們構造這樣一個簡單的鎖:其值為0表示鎖是開的(鎖可用),為1表示上鎖(不可用)。當處理器要給該鎖上鎖的時候,將對應於該鎖的儲存單元的值與存放在某個暫存器中的1進行交換。如果別的處理器已經上了鎖,那麼交換指令返回的值為1否則為0。返回0的時候,因為是原子交換,鎖的值就會從0變為1表示上鎖成功;返回1,原子交換鎖的值還是1,但是返回1表示已經被上了鎖。 我們考慮使用這個鎖:假設兩個處理器同時進行交換操作(原子交換),競爭的結果就是,只有一個處理器會先執行成功而得到返回值0,而另一個得到的返回值為1表示已經被上鎖。從這些我們可以看出,採用原子交換指令是實現同步的關鍵:這個原子交換操作的不可再分的,兩個交換操作將由寫順序機制確定先後順序,這也保證了兩個執行緒不能同時獲取同步變數鎖。

  除此之外,還有別的原語可以實現同步(關鍵都在於能以原子的方式讀-改-寫儲存單元的值)。例如:測試並置定(test_and_set)(先測試一個儲存單元的值,如果符合條件就修改其值),另一個同步原語是讀取並加1(fetch_and_increment))(返回儲存單元的值並自動增加該值)。(看到這裡可以回憶一下Java中的CAS操作的實現以及在Java中的實現原理)

  那麼,上面的基本原語操作又是怎樣實現的呢,這在一條指令中完成上述操作顯然是困難的(在一條不可中斷的指令中完成一次儲存器的讀改寫,而且要求不允許其他的訪存操作還要避免死鎖)。現在的計算機上採用一對指令來實現上述的同步原語。該指令對由兩條特殊的指令組成,一條是特殊的load指令(LL指令),另一條是特殊的store指令(SC)。指令的執行順序是:如果LL指令指明的儲存單元的值在SC對其進行寫之前被其他的指令改寫過,則第二條指令執行失敗,如果在兩條指令之間進行切換也會導致執行SC失敗,而SC指令將通過返回一個值來指出該指令操作是否成功(如果返回的1表示執行成功,返回0表示失敗)。為什麼說這對指令相當於原子操作呢,這指的是是所有其他處理器進行的操作或者在這對指令之前執行或者在其後執行,不存在兩條指令之間進行,所以在這一對指令之間不存在任何其他處理器改變相應儲存單元的值。

  下面是一段實現對R1指出的儲存單元進行的原子交換操作

1 try:OR    R3,R4,R0 //R4中為交換值,將該值送入R3
2     LL    R2,0(R1) //將0(R1)中的值取到R2
3     SC    R3,0(R1) //若0(R1)中的值與R3中的值相同,則置R3的值為1,否則為0
4     BEQZ R3,try //R3的值為0表示存失敗,轉移重新嘗試
5     MOV R4,R2 //成功,將取出的值送往R4     

  最終R4和由R1指向的儲存單元值進行了原子交換,在LL和SC之間如果有別的處理器插入並且修改了儲存單元的值則SC都會返回0並存入R3中從而重新執行交換操作。下面是實現各個講到的讀取並加1(fetch_and_increment)原語的實現

1 try:LL    R2,0(R1) //將0(R1)中的值送入R2
2     DADDIU    R2,R2,#1 //加1操作(R2+1->R2)
3     SC    R2,0(R1) //如果0(R1)中的值和R2中的值相同就置R2的值為1,否則為0
4     BEQZ    R2,try //R2的值為0表示存失敗,轉移到開始出重新執行

  上面的指令的執行需要跟蹤地址,通常LL指令指定一個暫存器,該暫存器中存放著目的儲存單元的地址,這個暫存器稱為連線暫存器,如果發生中斷切換或者與連線暫存器中的地址匹配的cache塊被作廢(被別的SC指令訪問),則將連線暫存器清零,SC指令則檢查它的儲存地址和連線暫存器匯中的內容是夠匹配,如果匹配則SC指令繼續執行,否則執行失敗。

2、用一致性實現鎖

  我們現在用上面的原子交換的同步原語實現自旋鎖(spin lock)(處理器不停請求獲得鎖的試用權,圍繞該鎖反覆執行迴圈程式,直到獲得鎖)。自旋鎖適用於這樣的場景:鎖被佔用時間少,在獲得鎖之後加鎖的過程延遲小。

  下面我們考慮使用一種簡單的方法實現:將鎖變數儲存在儲存器中,處理器可以不斷通過原子交換操作來請求其使用權,比如使用原子交換操作獲得其返回值從而直達鎖變數的使用情況。釋放鎖的時候,處理器只需要將說置為0。如下面的程式:使用原子交換操作堆自旋鎖進行加鎖,其中R1中存放的是自旋鎖變數的地址

1         DADDIU R2,R0,#1
2 lockit: EXCH R2,0(R1) //原子交換,獲得自旋鎖的值並在下面比較自旋鎖的值為1還是0,為1表示已經上鎖
3         BNEZ R2,lockit //若R2的內容不為0,則表示已經有其他程式獲得了鎖變數,就繼續旋轉等待

  下面我們對這個簡單的自旋鎖實現進行一些改進(下面說到的可類比JMM記憶體模型理解)如果計算機支援Cache一致性,就可以將鎖調入Cache中(類比本地記憶體),並通過一致性保證使得鎖的值保持和儲存器中的值一致(類比記憶體可見性和本地記憶體主記憶體的值一致同步)。這樣做有下面的好處:使得環繞自旋鎖的執行緒(自旋請求鎖變數)只對本地Cache中的鎖(主存中的副本)進行操作,而不用再每次請求佔用鎖時候進行一次全域性的訪存操作(訪問主記憶體儲存器中存放的鎖的值) 利用訪問鎖的程式區域性性原理(處理器最近使用的鎖可能不久後還會使用),這種情況就可以使得鎖駐留在對應的Cache中,大大減少了獲得鎖所需要的時間(處於效能考慮,需要減少全域性訪存操作)。

  在改進之前,我們應該知道,在上面的簡單實現的基礎上(上面的每次迴圈交換均需要一次寫操作,因為有多個處理器會同時請求加鎖,這就會導致一個處理器請求成功後,其他處理器都會寫不命中),需要對這個程式進行改進,使得它只對本地副本中的鎖變數進行讀取和檢測,直到發現鎖已經被釋放。發現釋放之後,立刻去進行交換操作跟別的處理器競爭鎖變數。所有這些程式還是以原子交換的方式獲得鎖,也只有一個程式可以獲得成功(獲得鎖變數成功的程式交換後看到的鎖變數值為0,交換之後的鎖變數值為1表示上鎖成功;而獲得失敗的程式雖然也交換了鎖變數的值,但是因為交換後自己看到的鎖變數的值已經是1,就表示自己程式失敗了),其他的需要繼續旋轉等待。當獲得鎖的程式使用完之後,將鎖變數置為0表示釋放鎖由其他需要獲取的程式去競爭它(其他程式會在自己的Cache中發現鎖變數的值發生變化,這是上面所說的Cache一致性)。下面是修改後的旋轉鎖程式

1 lockit: LD    R2,0(R1) //取得鎖的值
2         BNEZ R2,lockit //如果鎖還沒有釋放(R2的值還是1)
3         DADDIU    R2,R0,#1 //將R2值置為1(這裡面可以這樣想:上面BNEZ執行失敗表示R2值為0,那麼這個時候就+1)
4         EXCH R2,0(R1) //將R2中的值和0(R1)中的鎖變數進行原子交換
5         BNEZ R2,lockit //上面第一次判斷是當前程式首先發現主存中的鎖變數值發生變化;
6                        //進行原子交換結果判斷和上面一樣,如果狡猾後返回值為0表示成功,為1表示失敗就繼續旋轉等待獲取

3、使用上面的旋轉鎖實現我們一個同步原語——柵欄同步

  首先解釋一下什麼叫柵欄同步(barrier)。假設有一個類似於柵欄的東西,它會強制所有到達柵欄的程式進行等待,直到全部的程式都到達之後釋放所有到達的程式繼續往下執行,從而形成同步。下面我們就通過上面說的旋轉鎖來簡單模擬實現這樣的一個同步原語

  使用兩個旋轉鎖,一個表示計數器,記錄已經到達該柵欄的程式數;另一個用來封鎖程式知道最後一個程式到達該柵欄。為了實現柵欄,我們需要一個變數,到達並阻塞住的程式需要在這個變數上自旋等待知道滿足它需要的條件(都到達柵欄然後才能往下執行)。我們使用spin表示這個條件condition。如下的程式所示,其中lock和unlock提供基本的旋轉鎖,變數count記錄已經到達柵欄的程式數,total表示已經到達柵欄的程式總數,對counterlock加鎖保證了增量操作的原子性,release用來封鎖最後一個到達柵欄的程式。spin(release==1)表示需要全部程式都到達柵欄。

 1 lock(counterlock); //確保更新的原子性
 2 if(count == 0) release = 0; //第一個程式到達,這時候重置release為0表示在其值變為1之前後續到達的程式都需要等待
 3 count = count + 1; //記錄到達的程式數
 4 unlock(counterlock); //釋放鎖
 5 if(count == total) { //程式全部到達
 6     count = 0; //重置計數器count
 7     release = 1; //將release置為1表示釋放所欲到達的程式
 8 } else { //程式還沒有全部到達
 9     spin(release == 1); //已經到達的程式旋轉等待知道所有的程式到達(言外之意就是release=1)
10 }

  但是上面的這種簡單實現還是存在問題的,我們考慮下面這種可能發生的情況:當柵欄的使用在迴圈當中時候,這時候所有釋放的程式在執行一段時間之後還會到達柵欄,假設其中一個程式在上次釋放的時候還沒有來得及離開柵欄,而是依舊停留在旋轉操作上(可能作業系統重新進行程式排程導致那個程式沒有來得及離開柵欄)。如果第二次柵欄使用的時候,一個執行較快的程式到達柵欄(這個快的意思是,當他到達柵欄之後上次那個還沒有離開柵欄的程式還在旋轉操作上),這個快的程式會發現count=0,那麼他就會將release置為0,這時候就會導致那個還在旋轉等待的程式發現release值為0,然後那就更不會再退出這個旋轉操作了,就相當於被捆綁在柵欄上出不去(這個問題會導致後續的count計數少了一個程式到達,而總是小於total),那這樣的話,由於count總是小於total那不是所有到達柵欄的程式都在spin上一直自旋了嗎。那怎麼解決這個問題呢,一種方法就是在程式離開柵欄的時候也進行計數,在上次使用柵欄的程式全部離開柵欄之前不允許執行快的程式再次使用並初始化柵欄的一些變數值。還有一種方法是使用sense_reversing柵欄,即每個程式只用一個本地私有變數local_sense並初始化為1,用它和release判斷程式是否需要自旋等待。

二、Java中的原子性操作概述

  所謂原子操作,就是指執行一系列操作的時候,要麼全部執行要麼全部不執行,不存在只執行一部分的情況。在設定計數器的時候一般是讀取當前的值,然後+1在更新(讀-改-寫的過程),如果不能保證這這幾個操作的過程的原子性就可能出現執行緒安全問題,比如下面的程式碼示例,++value在沒有任何額外保證的前提下不是原子操作。

1 public class ThreadUnSafe{
2     private Long value;
3     
4     public Long getValue() {return value;}
5     
6     public void increment() {++value;}
7 }

  使用Javap -c XX.class檢視彙編程式碼如下

  這是個複合操作,是不具備原子性的。而保證這個操作原子性的方法最簡單的就是加上synchronized關鍵字(當然也可以是其他的加鎖操作,參考Java中的鎖——Lock和synchronized實現原理),使用synchronized可以實現執行緒安全性,但是這是個獨佔鎖,沒有獲取內部鎖的執行緒會被阻塞住(即便是這裡的getValue操作,多執行緒訪問也會阻塞住),這對於併發效能的提高是不好的(而這裡也不能簡單的去掉getValue上的synchronized,因為讀操作需要保證value的讀一致性,即需要獲得主記憶體中的值而不是執行緒工作記憶體中的可能是舊的副本值)。那麼除了加鎖之外其他安全的方法?後面講到的原子類(使用CAS實現)就可以作為一個選擇。

三、Java中的CAS操作概述

  Java中提供非阻塞的volatile關鍵字解決保證共享變數的可見性問題,但是不能解決部分符合操作不具備原子性的問題(比如自增運算)。CAS即CompareAndSwap是JDK提供的非阻塞原子操作,通過硬體保證比較更新的原子性。我們通過compareAndSwapLong來簡單介紹CAS:

  compareAndSwapLong(Object obj, long valueOffset, long expect, long update),該方法中compareAndSwap表示比較並交換,方法中有四個運算元,其中obj表示物件記憶體的位置,valueOffset表示物件中儲存變數的偏移量,expect表示變數的預期值,update表示更新值。操作含義就是,若果物件obj中記憶體偏移量為valueOffset的變數值為expect則使用心得update值替換舊的值expect,這是處理器提供的一個原子指令。這些方法有sun.misc.Unsafe類提供。後面我們會說到Unsafe類

  在此之前我們先說一下CAS操作的一個經典的ABA問題:假如執行緒1 使用CAS修改初始值為A的變數X,那麼執行緒1會首先回去當前變數X的值(A),然後使用CAS操作嘗試修改X的值為B,如果使用CAS修改成功了,那麼程式一定執行正確了嗎?在往下的假設看,如果執行緒I在獲取變數X的值A後,在執行CAS之前執行緒II使用CAS修改變數X的值為B然後由修改回了A。這時候雖然執行緒I執行CAS時候X的值依舊是A但是這個A已經不是執行緒I獲取時候的A了,這就是ABA問題。ABA產生的原因是變數的狀態值產生了環形轉換,即變數值從A->B,然後又從B->A。jdk中提供了帶有標記的原子類AtomicStampedReference(時間戳原子引用)通過控制變數的版本保證CAS的正確性。如下所做的測試ABA問題以及使用AtomicStampedReference來解決這個問題

  (1)模擬ABA問題,下面的程式輸出結果會是這樣的

 1 package test;
 2 
 3 import java.util.concurrent.TimeUnit;
 4 import java.util.concurrent.atomic.AtomicReference;
 5 
 6 public class TestAtomicStampedReference {
 7 
 8     static AtomicReference<Integer> atomicReference = new AtomicReference<>(1);
 9 
10     public static void main(String[] args) {
11         Thread t1 = new Thread(new Runnable() {
12             @Override
13             public void run() {
14                 atomicReference.compareAndSet(1,2);
15                 atomicReference.compareAndSet(2,1);
16                 System.out.println(Thread.currentThread() + "執行緒修改後的變數值" + atomicReference.get());
17             }
18         });
19 
20         Thread t2 = new Thread(new Runnable() {
21             @Override
22             public void run() {
23                 //sleep 1秒,保證執行緒t1完成1->2->1的模擬ABA操作
24                 try {
25                     TimeUnit.SECONDS.sleep(1);
26                 } catch (InterruptedException e) {
27                     e.printStackTrace();
28                 }
29                 atomicReference.compareAndSet(1,3);
30                 System.out.println(Thread.currentThread() + "執行緒修改後的變數值" + atomicReference.get());
31             }
32         });
33 
34         t1.start();
35         t2.start();
36     }
37 }

   (2)使用AtomicStampedReference重新實現,下面是執行結果

 1 package test;
 2 
 3 import java.util.concurrent.TimeUnit;
 4 import java.util.concurrent.atomic.AtomicStampedReference;
 5 
 6 public class TestAtomicStampedReference {
 7 
 8     static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10,1); //定義初始值和初始版本號
 9     public static void main(String[] args) {
10         Thread t1 = new Thread(new Runnable() {
11             @Override
12             public void run() {
13                 //執行緒1獲得初始版本號並sleep1秒
14                 int version = atomicStampedReference.getStamp();
15                 System.out.println(Thread.currentThread() + "當前執行緒獲得的版本號" + version);
16                 try {
17                     TimeUnit.SECONDS.sleep(1);
18                 } catch (InterruptedException e) {
19                     e.printStackTrace();
20                 }
21                 System.out.println(Thread.currentThread() + "修改變數結果true/false?:" +
22                         atomicStampedReference.compareAndSet(10,11,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1)
23                         + "修改後的結果:" + atomicStampedReference.getReference());
24                 System.out.println(Thread.currentThread() + "修改變數結果true/false?:" +
25                         atomicStampedReference.compareAndSet(11,10,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1)
26                         + "修改後的結果:" + atomicStampedReference.getReference());
27             }
28         });
29 
30         Thread t2 = new Thread(new Runnable() {
31             @Override
32             public void run() {
33                 //首先獲得初始版本號,sleep2秒讓執行緒1完成10->11->10的模擬ABA操作
34                 int version = atomicStampedReference.getStamp();
35                 System.out.println(Thread.currentThread() + "當前執行緒獲得的版本號" + version);
36                 try {
37                     TimeUnit.SECONDS.sleep(2);
38                 } catch (InterruptedException e) {
39                     e.printStackTrace();
40                 }
41                 System.out.println(Thread.currentThread() + "修改變數結果true/false?:" +
42                         atomicStampedReference.compareAndSet(10,20,version,atomicStampedReference.getStamp()+1)
43                         + "修改後的結果:" + atomicStampedReference.getReference());            
44             }
45         });
46 
47         t1.start();
48         t2.start();
49     }
50 }

四、Java中的Unsafe類

  JDK中的rt.jar包中的Unsafe類提供了硬體級別的原子性操作,Unsafe類中許多方法都是native方法,他們使用JNI的方式訪問本地C++中的實現庫。下面我們瞭解一下Unsafe類提供的幾個主要的方法以及如何使用unsafe類進行一些程式設計操作。

1、Unsafe類中的重要方法介紹

(1)public native long objectFieldOffset(Field var1):返回指定的變數在所屬類中的記憶體偏移地址,該偏移地址僅僅在該Unsafe函式中訪問指定欄位時候使用。如下使用Unsafe類獲取變數value在Atomic物件中的記憶體偏移量

 

(2)public native int arrayBaseOffset(Class<?> var1):獲取陣列中第一個元素的地址

(3)public native int arrayIndexScale(Class<?> var1):獲取陣列中一個元素佔用的位元組

(4)public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5):比較物件var1中的偏移量為var2的變數的值是否與var4相同,相同則使用var6的值更新,並返回true,否則返回false。

(5)public native long getLongVolatile(Object var1, long var2):獲取物件var1中偏移量為offset的變數對應volatile語義的值。

(6)public native void putLongVolatile(Object var1, long var2, long var4):設定var1物件中offset偏移型別為long的值為var4,支援volatile語義

(7)public native void putOrderedLong(Object var1, long var2, long var4):設定物件obj中offset偏移地址對應的long型的field的值為value。這是一個有延遲的putLongVolatile方法,並且不保證對應的值型別的修改對其他執行緒可見,只有變數在只用volatile修飾並且預計會被意外修改的時候才會使用該方法、

(8)public native void park(boolean var1, long var2):阻塞當前執行緒,其中引數var1等於false且var2等於0表示一直阻塞,var2大於0表示等待指定的時間後阻塞執行緒會被喚醒。這個var的值是相對的,為一個增量值,也就是相當當前時間累加事假後當前執行緒就會被喚醒。如果var1位true,並且var2大於0,則表示阻塞的執行緒到指定的時間點後就會被喚醒,這裡的時間var2是個絕對時間,是某個時間點換算為ms後的值。

(9)public native void unpark(Object var1):喚醒呼叫park方法之後的執行緒。

下面是jdk8之後新增加的,我們列出Long型別的方法

(10)getAndSetLong()方法:獲取當前物件var1中偏移量為var2的變數volatile語義的當前值,並設定變數volatile語義的值為var4。

 首先使用getLongVolatile獲取當前變數的值,然後使用CAS原子操作設定新的值。這裡使用while是當CAS失敗時候進行重試。

 

(11)getAndAddLong()方法:獲取物件var1中偏移量為var2變數的volatile語義的值,設定變數值為原始值+var4

 

2、Unsafe類的使用

  考慮編寫出下面的程式,並在自己的IDE中執行下面的程式,觀察結果。

 1 package test;
 2 
 3 import sun.misc.Unsafe;
 4 
 5 public class TestUnsafe {
 6 
 7     //獲取Unsafe的例項
 8     static Unsafe unsafe = Unsafe.getUnsafe();
 9 
10     //記錄變數value在TestUnsafe中的偏移量
11     static long valueState;
12 
13     //變數
14     private volatile long value;
15 
16     static {
17         try {
18             //獲取value變數在TestUnsafe類中的偏移量
19             valueState = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("value"));
20         } catch (Exception e) {
21             System.out.println(e.getMessage());
22         }
23     }
24 
25     public static void main(String[] args) {
26         TestUnsafe testUnsafe = new TestUnsafe();
27         System.out.println(unsafe.compareAndSwapInt(testUnsafe,valueState,0,1));
28     }
29 }

  上面的程式中首先獲取Unsafe的一個例項,然後使用unsafe的objectFieldOffset方法獲取TestUnsafe類中value變數,計算在TestUnsafe類中value變數的記憶體偏移地址並儲存到valueState中。main中呼叫unsafe的compareAndSwapInt方法設定testUnsafe物件的value變數的值為1(如果是0的話)。value初始預設是0,我們希望程式碼能輸出true(即compareAndSwapInt能夠執行成功),但是最終執行時下面的結果

   我們看到上面的異常報錯在getUnsafe方法位置,下來我們看一看getUnsafe方法

 1 public static Unsafe getUnsafe() {
 2     //(1)獲取呼叫getUnsafe類的這個Class類,按照上面的程式中的TestUnsafa類
 3     Class var0 = Reflection.getCallerClass();
 4     //(2)看下面的那個方法
 5     if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
 6         throw new SecurityException("Unsafe");
 7     } else {
 8         return theUnsafe;
 9     }
10 }
11 /**
12  * (3)判斷是不是啟動類載入器載入的類,即看看是不是由BootStrapClassLoader載入的TestUnsafe.class,
13  *    由於我們這是一個簡單測試類,是由應用程式類載入器AppClassLoader載入的,所以直接報出SecurityException異常
14  */
15 public static boolean isSystemDomainLoader(ClassLoader var0) {
16     return var0 == null;
17 }

  由於Unsafe類rt.jar包提供的,該包下面的類都是通過Bootstrap類載入器載入的,而我們使用的main方法所在的類是由AppClassLoader載入的,所以在main方法中載入Unsafe類的時候根據雙親委派機制會委託給Bootstrap載入。那麼如果想要使用Unsafe類應該怎樣使用呢,《深入理解java虛擬機器》中這一塊告訴我們可以使用反射來使用,下面我們來試一下

 1 package test;
 2 
 3 import sun.misc.Unsafe;
 4 
 5 import java.lang.reflect.Field;
 6 
 7 public class TestUnsafe2 {
 8 
 9     static Unsafe unsafe;
10 
11     static long valueOffset;
12 
13     private volatile long value = 0;
14 
15     static {
16         try {
17             //使用反射獲取Unsafe的成員變數theUnsafe
18             Field field = Unsafe.class.getDeclaredField("theUnsafe");
19             //設定為課存取
20             field.setAccessible(true);
21             //設定該變數的值
22             unsafe = (Unsafe) field.get(null);
23             //獲取value偏移量
24             valueOffset = unsafe.objectFieldOffset(TestUnsafe2.class.getDeclaredField("value"));
25         } catch (NoSuchFieldException e) {
26             e.printStackTrace();
27         } catch (IllegalAccessException e) {
28             e.printStackTrace();
29         }
30     }
31 
32     public static void main(String[] args) {
33         TestUnsafe2 test = new TestUnsafe2();
34         System.out.println("修改變數結果true/false?:" +
35                 unsafe.compareAndSwapInt(test,valueOffset,0,1)
36                 + "修改後的結果:" + test.value);
37     }
38 }

  得到下面的結果:

五、JUC中原子操作類AtomicLong的原理探究

1、原操作類概述

  JUC包中提供了很多原子操作類,這些類都是通過上面說到的非阻塞CAS演算法來實現的,相比較使用鎖來實現原子性操作CAS在效能上有很大提高。由於原子操作類的原理都大致相同,所以下面分析AtomicLong類的實現原理來進一步瞭解原子操作類。

2、AtomicLong的原始碼

  下面是AtomicLong原子類的部分原始碼,其中主要包含其成員變數以及一些靜態程式碼塊和構造方法

 1 public class AtomicLong extends Number implements java.io.Serializable {
 2 
 3     //(1)獲取Unsafe例項
 4     private static final Unsafe unsafe = Unsafe.getUnsafe();
 5     
 6     //(2)儲存value值的偏移量
 7     private static final long valueOffset;
 8     
 9     //(3)判斷當前JVM是否支援Long型別的無鎖CAS
10     static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
11     private static native boolean VMSupportsCS8();
12     
13     static {
14         try {
15             //(4)獲取value值在AtomicLong中的偏移量
16             valueOffset = unsafe.objectFieldOffset
17                 (AtomicLong.class.getDeclaredField("value"));
18         } catch (Exception ex) { throw new Error(ex); }
19     }
20     
21     //(5)實際存的變數值value
22     private volatile long value;
23 
24     //構造方法
25     public AtomicLong(long initialValue) {
26         value = initialValue;
27     }
28 }

  在上面的部分程式碼中,程式碼(1)通過Unsafe.getUnsafe()方法獲取到Unsafe類的例項(AtomicLong類也是rt.jar包下面的,所以AtomicLong也是通過啟動類載入器進行類載入的)。(2)(4)兩處是計算並儲存AtomicLong類中儲存的變數value的偏移量。(5)中的value被宣告為volatile的(關於volatile的記憶體語義以及實現原理參考前面寫到的Java併發程式設計基礎之volatile),這是為了在多執行緒下保證記憶體的可見性,而value就是具體存放計數的變數。下面我們看看AtomicLong中的主要幾個函式

  (1)遞增和遞減的原始碼

 1 //使用unsafe的方法,原子性的設定value值為原始值+1,返回值為遞增之後的值
 2 public final long getAndIncrement() {
 3     return unsafe.getAndAddLong(this, valueOffset, 1L);
 4 }    
 5 public final long incrementAndGet() {
 6     return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
 7 }
 8 //使用unsafe的方法,原子性的設定value值為原始值-1,返回值為遞減之後的值
 9 public final long getAndDecrement() {
10     return unsafe.getAndAddLong(this, valueOffset, -1L);
11 }
12 public final long decrementAndGet() {
13     return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
14 }

  在上面的程式碼中都是通過呼叫Unsafe類的getAndAddLong方法來實現操作的,我們來看看這個方法,這個方法是個原子性操作:其中的第一個引數是AtomicLong例項的引用,第二個引數是value變數在AtomicLong中的偏移量,第三個引數是要設定為第二個變數的值。下面就是getAndAddLong方法的實現,以及一些分析

 1 public final long getAndAddLong(Object var1, long var2, long var4) {
 2     long var6;
 3     do {
 4         //public native long getLongVolatile(Object var1, long var2);
 5         //該方法就是獲取var1引用指向的記憶體地址中偏移量為var2位置的值,然後賦給var6
 6         var6 = this.getLongVolatile(var1, var2);
 7     /**public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
 8      * var1:AtomicXXX型別的一個引用,指向堆記憶體中的一塊地址
 9      * var2:AtomicXXX原始碼中的valueOffset,表示AtomicXXX原始碼中實際儲存的值value在原子型別記憶體中的地址偏移量
10      * var4:要比較的目標值expectValue,如果從記憶體指定地址處(var1和var2決定的那塊地址)的值和該值相等,則CAS成功
11      * var6:CAS成功後向該記憶體中寫進的新值
12      */
13     //該方法就是使用CAS的方式,比較指定記憶體地址處(var1指向的記憶體地址塊中偏移量為var2處)的值和上面同一塊地址處取出的var6是否相等,
14     //相等就將var6+var4(這裡可以看成var6+1)和指定記憶體地址處(var2引用指向的地址塊中偏移量為var2處)的值交換,並返回true,然後就會結束迴圈
15     //CAS失敗返回false,然後繼續執行迴圈體內部的程式碼,直到成功(也就是自增運算成功就會跳出迴圈並返回自增後的值)
16     } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
17 
18     return var6;
19 }

  (2)CompareAndSet方法

  下面是compaerAndSet方法的實現,主要還是呼叫unsafe類的compareAndSwapLong方法,其原理和上面分析的差不多,都是通過CAS的方式進行比較交換值。

1 public final boolean compareAndSet(long expect, long update) {
2     //public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
3     return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
4 }

  (3)擴充套件,下面是compareAndSwapInt的底層實現,實際上是通過硬體同步原語來實現的CAS,下面的cmpxchg就是基於硬體原語實現的

1 UNSAFE_ENTRY(jboolean,Usafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
2 UsafeWrapper("Usafe_CompareAndSwapInt");
3 oop p = JNIHasdles::resolve(obj);
4 jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
5 return (jint)(Atomic::cmpxchg(x,addr,e)) == e;
6 UNSAFE_END

  (4)下面是一個例子,使用AtomicLong來進行技術運算

 1 package test;
 2 
 3 import java.util.concurrent.atomic.AtomicLong;
 4 
 5 public class TestAtomic1 {
 6 
 7     //建立AtomicLong型別的計數器
 8     private static AtomicLong atomicLong = new AtomicLong();
 9 //    private static Long atomicLong = 0L;
10     //建立兩個陣列,計算陣列中的0的個數
11     private static Integer[] arr1 = {0,1,2,3,0,5,6,0,56,0};
12     private static Integer[] arr2 = {10,1,2,3,0,5,6,0,56,0};
13 
14     public static void main(String[] args) throws InterruptedException {
15 
16         //執行緒1統計arr1中0的個數
17         Thread t1  = new Thread(new Runnable() {
18             @Override
19             public void run() {
20                 int size = arr1.length;
21                 for (int i = 0; i < size; i++) {
22                     if(arr1[i].intValue() == 0) {
23 //                        atomicLong.getAndIncrement();
24                         atomicLong++;
25                     }
26                 }
27             }
28         });
29 
30         Thread t2  = new Thread(new Runnable() {
31             @Override
32             public void run() {
33                 int size = arr2.length;
34                 for (int i = 0; i < size; i++) {
35                     if(arr2[i].intValue() == 0) {
36 //                        atomicLong.getAndIncrement();
37                         atomicLong++;
38                     }
39                 }
40             }
41         });
42 
43         t1.start();
44         t2.start();
45 
46         t1.join();
47         t2.join();
48 
49         System.out.println("兩個陣列中0出現的次數為: " + atomicLong);//兩個陣列中0出現的次數為: 7
50     }
51 }

  如果沒有使用原子型別進行計數運算,那麼可能就是下面的結果

   

相關文章