歡迎光臨
每天分享高質量文章

對 volatile、compareAndSet、weakCompareAndSet 的一些思考

(點選上方公眾號,可快速關註)


來源:tomas家的小撥浪鼓 ,

www.jianshu.com/p/55a66113bc54

最近在看AtomicIntegerFieldUpdater的時候看到了兩個很有意思的方法:compareAndSet 和 weakCompareAndSet。下麵主要針對這兩個方法展開討論。

基於 JDK 8

首先,我們知道AtomicIntegerFieldUpdater是一個基於反射的功能包,它可以實現針對於指定類中volatile int 欄位的原子更新。

『 compareAndSet 』:

/**

 * Atomically sets the field of the given object managed by this updater

 * to the given updated value if the current value {@code ==} the

 * expected value. This method is guaranteed to be atomic with respect to

 * other calls to {@code compareAndSet} and {@code set}, but not

 * necessarily with respect to other changes in the field.

 *

 * @param obj An object whose field to conditionally set

 * @param expect the expected value

 * @param update the new value

 * @return {@code true} if successful

 * @throws ClassCastException if {@code obj} is not an instance

 * of the class possessing the field established in the constructor

 */

public abstract boolean compareAndSet(T obj, int expect, int update);

以原子的方式更新這個更新器所管理的物件(obj)的成員變數,並且將這個成員變數更新為給定的更新後的值(update)如果當前值等於期望值(expect)時。

當存在其他使用‘compareAndSet’或者’set’的情況下,這個方法可以確保是原子的,但如果你用其他的方式去改變這個成員變數時(如,使用直接賦值的方式 field=newField),那麼它是不會遵循這個原子性的。

嗯,這個方法好理解,compareAndSet保證了:a) 只有field的值為expect時;b) 將field的值修改為update的值;這兩步是原子完成的。同時field一定為一個volatile屬性,而volatile保證了屬性在執行緒間的可見性,以及防止了指令的重排序。(關於volatile下麵還會進一步展開)。嗯,一切看起來都挺美好的。

然後,我們來看下另一個方法『weakCompareAndSet』:

/**

 * Atomically sets the field of the given object managed by this updater

 * to the given updated value if the current value {@code ==} the

 * expected value. This method is guaranteed to be atomic with respect to

 * other calls to {@code compareAndSet} and {@code set}, but not

 * necessarily with respect to other changes in the field.

 *

 *

May fail

 * spuriously and does not provide ordering guarantees

, so is

 * only rarely an appropriate alternative to {@code compareAndSet}.

 *

 * @param obj An object whose field to conditionally set

 * @param expect the expected value

 * @param update the new value

 * @return {@code true} if successful

 * @throws ClassCastException if {@code obj} is not an instance

 * of the class possessing the field established in the constructor

 */

public abstract boolean weakCompareAndSet(T obj, int expect, int update);

以原子的方式更新這個更新器所管理的物件(obj)的成員變數,並且將這個成員變數更新為給定的更新後的值(update)如果當前值等於期望值(expect)時。

當存在其他使用‘compareAndSet’或者’set’的情況下,這個方法可以確保是原子的,但如果你用其他的方式去改變這個成員變數時(如,使用直接賦值的方式 field=newField),那麼它是不會遵循這個原子性的。

該方法可能可能虛假的失敗並且不會提供一個排序的保證,所以它在極少的情況下用於代替compareAndSet方法。

第一次看weakCompareAndSet doc檔案的說明時,我是困惑的。我並不清楚你說的“fail spuriously”和“not provide ordering guarantees”的確切含義。於是我查詢了些相關資料。

首先,我從jdk 8 的官方檔案的java.util.concurrent.atomic上找到這麼二段話:

The atomic classes also support method weakCompareAndSet, which has limited applicability. On some platforms, the weak version may be more efficient than compareAndSet in the normal case, but differs in that any given invocation of the weakCompareAndSet method may return false spuriously (that is, for no apparent reason). A false return means only that the operation may be retried if desired, relying on the guarantee that repeated invocation when the variable holds expectedValue and no other thread is also attempting to set the variable will eventually succeed. (Such spurious failures may for example be due to memory contention effects that are unrelated to whether the expected and current values are equal.) Additionally weakCompareAndSet does not provide ordering guarantees that are usually needed for synchronization control. However, the method may be useful for updating counters and statistics when such updates are unrelated to the other happens-before orderings of a program. When a thread sees an update to an atomic variable caused by a weakCompareAndSet, it does not necessarily see updates to any other variables that occurred before the weakCompareAndSet. This may be acceptable when, for example, updating performance statistics, but rarely otherwise.

一個原子類也支援weakCompareAndSet方法,該方法有適用性的限制。在一些平臺上,在正常情況下weak版本比compareAndSet更高效,但是不同的是任何給定的weakCompareAndSet方法的呼叫都可能會傳回一個虛假的失敗( 無任何明顯的原因 )。一個失敗的傳回意味著,操作將會重新執行如果需要的話,重覆操作依賴的保證是當變數持有expectedValue的值並且沒有其他的執行緒也嘗試設定這個值將最終操作成功。( 一個虛假的失敗可能是由於記憶體衝突的影響,而和預期值(expectedValue)和當前的值是否相等無關 )。此外weakCompareAndSet並不會提供排序的保證,即通常需要用於同步控制的排序保證。然而,這個方法可能在修改計數器或者統計,這種修改無關於其他happens-before的程式中非常有用。當一個執行緒看到一個透過weakCompareAndSet修改的原子變數時,它不被要求看到其他變數的修改,即便該變數的修改在weakCompareAndSet操作之前。

weakCompareAndSet atomically reads and conditionally writes a variable but does not create any happens-before orderings, so provides no guarantees with respect to previous or subsequent reads and writes of any variables other than the target of the weakCompareAndSet.

weakCompareAndSet實現了一個變數原子的讀操作和有條件的原子寫操作,但是它不會建立任何happen-before排序,所以該方法不提供對weakCompareAndSet操作的標的變數以外的變數的在之前或在之後的讀或寫操作的保證。

這二段話是什麼意思了,也就是說weakCompareAndSet底層不會建立任何happen-before的保證,也就是不會對volatile欄位操作的前後加入記憶體屏障。因為就無法保證多執行緒操作下對除了weakCompareAndSet操作的標的變數( 該標的變數一定是一個volatile變數 )之其他的變數讀取和寫入資料的正確性。

這裡,需要對volatile進行一個較為詳細的說明。這樣大家就能更深刻的明白上面這段話的語意了。

volatile

volatile 的特性

volatile變數自身具有下列特性:

① 可見性/一致性:對一個 volatile 變數的讀,總是能看到(任意執行緒)對這個 volatile 變數最後的寫入。

② 原子性:對任意單個 volatile 變數的讀/寫具有原子性,但類似於 volatile++這種複合操作不具有原子性。

Q:volatile是如何保證可見性的了?

A:在多核處理器中,當進行一個volatile變數的寫操作時,JIT編譯器生成的彙編指令會在寫操作的指令前在上一個“lock”字首。“lock”字首的指令在多核處理器下會引發了兩件事情:

① 將當前處理器快取行的資料會寫回到系統記憶體。

② 這個寫回記憶體的操作會引起在其他CPU裡快取了該記憶體地址的資料無效。

因此更確切的來說,因為操作快取的最小單位為一個快取行,所以每次對volatile變數自身的操作,都會使其所在快取行的資料會寫回到主存中,這就使得其他任意執行緒對該快取行中變數的讀操作總是能看到最新寫入的值( 會從主存中重新載入該快取行到執行緒的本地快取中 )。當然,也正是因為快取每次更新的最小單位為一個快取行,這導致在某些情況下程式可能出現“偽共享”的問題。嗯,好像有些個跑題,“偽共享”並不屬於本文範疇,這裡就不進行展開討論。

好了,目前為止我們已經瞭解volatile變數自身所具有的特性了。註意,這裡只是volatile自身所具有的特性,而volatile對執行緒的記憶體可見性的影響比volatile自身的特性更為重要。

volatile 寫-讀建立的 happens before 關係

happens-before 規則中有這麼一條:

volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。

happens-before的這個規則會保證volatile寫-讀具有如下的記憶體語意:

volatile寫的記憶體語意:

當寫一個 volatile 變數時,JMM 會把該執行緒對應的本地記憶體中的共享變數值掃清到主記憶體。

volatile讀的記憶體語意:

當讀一個 volatile 變數時,JMM 會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數。

為了實現 volatile 的記憶體語意,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。因為記憶體屏障是一組處理器指令,它並不由JVM直接暴露,因此JVM會根據不同的作業系統插入不同的指令以達成我們所要記憶體屏障效果。

從整體執行效率的角度考慮,JMM 選擇了在每個 volatile 寫的後面插入一個 StoreLoad 屏障。

StoreLoad屏障

指令示例:Store1; StoreLoad; Load2

確保Store1資料對其他處理器變得可見(指掃清到記憶體)先於Load2及所有後續裝載指令的裝載。StoreLoad Barriers會使該屏障之前的所有記憶體訪問指令(儲存和裝載指令)完成之後,才執行該屏障之後的記憶體訪問指令。StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果(LoadLoad Barriers、StoreStore Barriers、LoadStore Barriers)

好了,到現在我們知道了volatile的記憶體語意( happens-before關係 )會保證volatile寫操作之前的讀寫操作不會被重排序到volatile寫操作之後,並且保證了寫操作後將執行緒本地記憶體(可能包含了多個快取行)中所有的共享變數值都掃清到主記憶體中。這樣其他執行緒總是能在volatile寫操作後的讀取操作中得到該執行緒中所有共享變數的正確值。這是volatile的happens-before關係( 透過記憶體屏障實現 )帶給我們的結果。註意,這個和volatile變數自身的特性是不同的,volatile自身僅僅是保證了volatile變數本身的可見性。而volatile的happens-before關係則保證了操作不會被重排序同時保證了執行緒本地記憶體中所有共享變數的可見性。

好了,討論到這裡,我們重新來理解下weakCompareAndSet的實現語意。也就是說,weakCompareAndSet操作僅保留了volatile自身變數的特性,而出去了happens-before規則帶來的記憶體語意。也就是說,weakCompareAndSet無法保證處理操作標的的volatile變數外的其他變數的執行順序( 編譯器和處理器為了最佳化程式效能而對指令序列進行重新排序 ),同時也無法保證這些變數的可見性。

原始碼實現

目前為止,我們已經能夠明白compareAndSet方法和weakCompareAndSet方法的不同之處了。那麼,接下來我們來看看這兩個方法的具體實現:

public boolean compareAndSet(T obj, int expect, int update) {

    if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);

    return unsafe.compareAndSwapInt(obj, offset, expect, update);

}

 

public boolean weakCompareAndSet(T obj, int expect, int update) {

    if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);

    return unsafe.compareAndSwapInt(obj, offset, expect, update);

}

是的,你沒有看錯。這兩個方法的實現完全一樣。『unsafe.compareAndSwapInt(obj, offset, expect, update);』中就是呼叫native方法了:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {

     int mp = os::is_MP();

     __asm__ volatile (LOCK_IF_MP(%4) “cmpxchgl %1,(%3)”

                     : “=a” (exchange_value)

                     : “r” (exchange_value), “a” (compare_value), “r” (dest), “r” (mp)

                      : “cc”, “memory”);

     return exchange_value;

}

由此可見,在JDK8乃至之前的版本,weakCompareAndSet方法並沒有被真是意義上的實現,目前該方法所呈現出來的效果與compareAndSet方法是一樣的。

基於JDK 9

在JDK 9中 compareAndSet 和 weakCompareAndSet方法的實現有些許的不同

/**

 * Atomically updates Java variable to {@code x} if it is currently

 * holding {@code expected}.

 *

 *

This operation has memory semantics of a {@code volatile} read

 * and write.  Corresponds to C11 atomic_compare_exchange_strong.

 *

 * @return {@code true} if successful

 */

@HotSpotIntrinsicCandidate

public final native boolean compareAndSetInt(Object o, long offset,

                                             int expected,

                                             int x);

① 底層呼叫的native方法的實現中,cmpxchgb指令前都會有“lock”字首了(在JDK 8中,程式會根據當前處理器的型別來決定是否為cmpxchg指令新增lock字首。只有在CPU是多處理器(multi processors)的時候,會新增一個lock字首)。

② 同時多了一個@HotSpotIntrinsicCandidate註解,該註解是特定於Java虛擬機器的註解。透過該註解表示的方法可能( 但不保證 )透過HotSpot VM自己來寫彙編或IR編譯器來實現該方法以提供效能。

它表示註釋的方法可能(但不能保證)由HotSpot虛擬機器內在化。如果HotSpot VM用手寫彙編和/或手寫編譯器IR(編譯器本身)替換註釋的方法以提高效能,則方法是內在的。

也就是說雖然外面看到的在JDK9中weakCompareAndSet和compareAndSet底層依舊是呼叫了一樣的程式碼,但是不排除HotSpot VM會手動來實現weakCompareAndSet真正含義的功能的可能性。

後記

嗯,關於compareAndSet與weakCompareAndSet兩個方法的不同,看似可能是個“簡單”的問題,但當我真的去探究它們的不同時,還是話費了我不少的時間,同時也讓我對volatile有了更加深入的理解。這裡關於CAS還有不少值得深入探討的地方,值得再用一篇文章好好的進行敘說。關於JDK9的改變也是值得以後慢慢去探索的。

參考

  • 《Java 併發程式設計的藝術》

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂