【Java併發程式設計的藝術】第二章讀書筆記之原子操作

凱倫說_美團點評發表於2019-03-04

前言

今天的筆記來了解一下原子操作以及Java中如何實現原子操作。

概念

原子(atomic)本意是“不能被進一步分割的最小粒子”,而原子操作(atomic operation)意為“不可被中斷的一個或一系列操作”。

處理器實現原子操作

處理器會保證基本記憶體操作的原子性。處理器保證從系統記憶體中讀取或者寫入一個位元組是原子的,意思是當一個處理器讀取一個位元組時,其他處理器不能訪問這個位元組的記憶體地址。最新的處理器能自動保證單處理器進行16/32/64位的操作是原子的,並且提供匯流排鎖定和快取鎖定兩個機制來保證複雜記憶體操作的原子性。

使用匯流排保證原子性

如果有多個處理器同時對共享變數進行操作,那麼共享變數就會被多個處理器同時操作,這樣的話,讀改寫操作就不是原子的。
比如i=1,i++,兩個處理器同時進行操作,最後的結果,可能是3,也可能是3.
原因可能是多個處理器同時從各自的快取中讀取變數i,分別進行加1操作,然後分別寫入系統記憶體。
處理器使用匯流排鎖來解決這個問題。當處理器發出LOCK#訊號時,其他處理器的請求會被阻塞主,該處理器可以獨佔共享記憶體。

使用快取鎖定來保證原子性

鎖匯流排開銷還是很大的,鎖住了CPU和記憶體之間的通訊。
因為頻繁使用的記憶體會快取在處理器的L1、L2、L3快取記憶體中,原子操作可以在快取內部完成,同時通過快取一致性協議,當A處理器修改快取中的i時,其他處理器不能同時快取i,即會使得其他處理器中對於共享變數的快取失效。
這段還不是特別明白,感覺得重新翻一下作業系統,有知道的網友可以留言補充一下。

Java實現原子操作的方式

Java可以使用鎖,實現一段程式碼的原子操作。但這樣開銷比較大,會引起頻繁的上下文切換。
另外一種方式就是使用CAS操作(比較交換)。
CAS演算法的過程是比較簡單的。它會包含三個引數(V,E,N))。V表示要更新的變數,E表示預期值,N值。當且僅當V等於E值時,才會將V的值設為N,如果V值和E值不同,說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。
當多個執行緒同時使用CAS對變數進行操作時,只有一個會勝出併成功更新,其餘會失敗。失敗的執行緒不會被掛起。
Java中對於基本型別的包裝類都有對應的原子操作實現,比如
AtomicBoolean
AtomicInteger

如果拿AtomicInteger為例子,其中的incrementAndGet的實現如下所示,是直接呼叫了Unsafe類的方法:

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }複製程式碼

其中var1是傳入物件的引用,var2是欄位到物件頭部的偏移量,方便快速定位,var5是當前值,var5+var4就是期望值,var4傳入的是1。
如果是引用型別的話,可以使用AtomicReference。
CAS操作雖好,但它會遇到ABA問題,即一個變數先是A,後來變成了B,在比較時又變回了A,但CAS操作無法感知到這種情況,如果說我們是否可以修改當前值,不僅取決於當前值,還取決於它的變化,那麼原有的CAS操作就無能為力了,因為它感知不到。
貼心的JDK為我們提供了AtomicStampedReference,它在物件內部維護了時間戳,當更新資料時,不僅要更新資料,還要更新時間戳。當AtomicStampedReference設定物件值時,物件值以及時間戳都必須滿足期望值,寫入才會成功。
如果是陣列型別的話,JDK提供了AtomicIntegerArray等陣列型別的原子類。

相關文章