併發編程系列之原子操作實現原理

前言

上節我們講了併發編程中最基本的兩個元素的底層實現,同樣併發編程中還有一個很重要的元素,就是原子操作,原子本意是不可以再被分割的最小粒子,原子操作就是指不可中斷的一個或者一系列操作。那麼今天我們就來看看在多處理器環境下Java是如何保證原子操作的,ok,開始我們今天的併發編程之旅吧。

 

處理器如何實現原子操作

處理器自身會自動保證基本的內存操作原子性,保證從系統內存中讀取或者寫入一個字節動作是原子性的,也就是說,當處理器讀取一個字節時,其他處理器不能訪問該字節的內存地址。但是處理器自身只能保證基本的內存操作原子性,對於複雜的操作例如跨總線、跨多個緩存行和跨頁表的訪問,處理器自身是無法保證原子操作的,只能通過下面兩種機制來保證操作原子性:

 

使用總線鎖保證原子性:假設多個處理器同時處理一個共享變量,如果沒有鎖機制,就會出現同一個共享變量同一時刻被多個處理器同時處理的情況,就會造成結果的不一致性,例如i=1,我們要進行2次i++,就會出現i=2和i=3兩種結果情況。很明顯這不是我們要的結果,所以要想保證讀寫共享變量的操作是原子性的,就必須保證當處理器1在操作共享變量時,處理器2不能操作緩存了該共享變量內存地址的緩存。

因此引出了總線鎖,總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號,其他處理器的請求將被阻塞,那麼該處理器就能獨佔共享內存;

可以想象爲:公司有一個會議室(共享內存),各個部門(處理器)開會都在會議室進行,當有一個部門佔用會議室時,就會在會議室門口或者公司羣裏通知會議室此時被佔用,那麼其他部門的會議就得先等着(阻塞),等該部門結束會議才能開始下一個會議。

 

總線鎖的缺點:總線鎖把CPU和內存之間的通信鎖住了,在鎖期間,其他處理器不能操作其他內存地址的數據,開銷比較大。

  • 使用緩存鎖保證原子性:緩存鎖指的是內存區域如果被緩存在處理器的緩存行中,並且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫到內存時,處理器不在總線上通知LOCK信號,而是修改內部的內存地址,然後通過它的緩存一致性機制來保證操作的原子性;

    緩存一致性機制:緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據,當其他處理器回寫被已經鎖定的緩存行數據時,會使緩存行失效。

  • 兩種情況下不使用緩存鎖

    • 當操作的數據不能被緩存在處理器內部,或者操作的數據跨多個緩存行,此時直接用總線鎖

    • 當處理器不支持緩存鎖定時,即使鎖定的內存區域在處理器緩存行中,也會直接使用總線鎖

 

Java中如何實現原子操作

Java中主要通過下面兩種方式來實現原子操作:鎖和循環CAS

鎖機制實現原子操作

鎖機制保證了只有獲得鎖的線程才能操作鎖定的內存區域,JVM內部實現了很多種鎖,偏向鎖、輕量級鎖和互斥鎖,等等,這裏就先不對鎖進行詳細介紹了,偏向鎖和輕量級鎖,上文也有提到過,感興趣的可以通過文章末尾點擊查閱,這些鎖中,除了偏向鎖,其他鎖的方式都使用了循環CAS,那麼我們來看下CAS相關內容。

 

什麼是CAS操作

CAS全稱Compare-and-Swap(比較並交換),JVM中的CAS操作是依賴處理器提供的cmpxchg指令完成的,CAS指令中有3個操作數,分別是內存位置V、舊的預期值A和新值B。

 

CAS指令如何保證原子性

當CAS指令執行時,當且僅當內存位置V符合舊預期值時A時,處理器纔會用新值B去更新V的值,否則就不執行更新,但是無論是否更新V,都會返回V的舊值,該操作過程就是一個原子操作,JDK1.5之後纔可以使用CAS,由sun.misc.Unsafe類裏面的compareAndSwapInt()和compareAndSwapLong()等方法包裝實現,虛擬機在即時編譯時,對這些方法做了特殊處理,會編譯出一條相關的處理器CAS指令;

但是由Unsafe方法是虛擬機自己調用的,不能直接供用戶程序調用,我們只能通過J.U.C包下的類來間接調用,如AtomicInteger和AtomicLong類,這些類中的方法都以原子的方式來進行操作,例如AtomicInteger的incrementAndGet方法中就是使用了compareAndSet,compareAndSet和getAndIncrement等方法都是使用Unsafe的CAS操作;

public final int incrementAndGet() {
       for (;;) {
           int current = get();
           int next = current + 1;
           if (compareAndSet(current, next))
               return next;
       }
   }

先獲取到當前的 value 屬性值,然後將 value 加 1,賦值給一個局部的 next 變量,然而,這兩步都是非線程安全的,但是內部有一個死循環(循環CAS或者稱自旋CAS),不斷去做compareAndSet操作,直到成功爲止,也就是修改的根本在compareAndSet方法裏面

public final boolean compareAndSet(int expect, int update) {
       return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
   }

compareAndSwapInt上面提到了是Unsafe類的方法,是基於CAS指令的,其實compareAndSwapInt方法的聲明是一個native本地方法

publicfinal native boolean compareAndSwapInt(Object var1, long var2, int var4, intvar5);

 

CAS實現操作的三大問題

CAS雖然很好的解決了原子操作問題,但是仍然存在下面3種問題

ABA問題:使用CAS時因爲會先去檢查內存位置的舊值A有沒有發生變化,發生變化則更新最新值B,但是存在一種情況就是,初次讀取內存舊值時是A,再次檢查之前這段期間,如果內存位置的值發生過從A變成B再變回A的過程,我們就會錯誤的檢查到舊值還是A,認爲沒有發生變化,其實已經發生過A-B-A得變化,這就是CAS操作的ABA問題;

解決ABA問題的思路:使用版本號,即1A-2B-3A,這樣就會發現1A到3A的變化,不存在ABA變化無感知問題,JDK的atomic包中提供一個帶有標記的原子引用類AtomicStampedReference來解決ABA問題,它可以通過控制變量值得版本號來保證CAS的正確性。該類的compareAndSet方法會首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果都相等則以原子方式該引用和該標誌的值進行更新。

循環時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷;

只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,可以使用循環CAS來保證原子操作,但是多個共享變量操作時,就無法保證了。

解決方法

  • 使用鎖

  • 將多個變量組合成一個共享變量,jdk提供了AtomicReference類來保證引用對象之間的原子性,那麼就可以把多個變量放在一個對象裏來進行CAS操作

 

結合上節內容,我們就將併發底層最重要的三個元素實現原理講了一遍,這裏面或多或少有些概念或者操作,大家看得有些模糊,不用擔心,後續我們還會繼續探討

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章