Java併發基礎知識(三)

CAS基本原理

什麼是原子操作?如何實現原子操作?

假定有兩個操作A和B(A和B可能都很複雜),如果從執行A的線程來看,當另一個線程執行B時,要麼將B全部執行完,要麼完全不執行B,那麼A和B對彼此來說是原子的。

實現原子操作可以使用鎖,鎖機制,滿足基本的需求是沒有問題的了,但是有的時候我們的需求並非這麼簡單,我們需要更有效,更加靈活的機制,synchronized關鍵字是基於阻塞的鎖機制,也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖,

這裏會有些問題:首先,如果被阻塞的線程優先級很高很重要怎麼辦?其次,如果獲得鎖的線程一直不釋放鎖怎麼辦?(這種情況是非常糟糕的)。還有一種情況,如果有大量的線程來競爭資源,那CPU將會花費大量的時間和資源來處理這些競爭,同時,還有可能出現一些例如死鎖之類的情況,最後,其實鎖機制是一種比較粗糙,粒度比較大的機制,相對於像計數器這樣的需求有點兒過於笨重。

實現原子操作還可以使用當前的處理器基本都支持CAS()的指令,只不過每個廠家所實現的算法並不一樣,每一個CAS操作過程都包含三個運算符:一個內存地址V,一個期望的值A和一個新值B,操作的時候如果這個地址上存放的值等於這個期望的值A,則將地址上的值賦爲新值B,否則不做任何操作。

CAS的基本思路就是,如果這個地址上的值和期望的值相等,則給其賦予新值,否則不做任何事兒,但是要返回原值是多少。循環CAS就是在一個循環裏不斷的做cas操作,直到成功爲止。

CAS實現原子操作的三大問題

ABA問題。

因爲CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。

ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。舉個通俗點的例子,你倒了一杯水放桌子上,幹了點別的事,然後同事把你水喝了又給你重新倒了一杯水,你回來看水還在,拿起來就喝,如果你不管水中間被人喝過,只關心水還在,這就是ABA問題。

如果你是一個講衛生講文明的小夥子,不但關心水在不在,還要在你離開的時候水被人動過沒有,因爲你是程序員,所以就想起了放了張紙在旁邊,寫上初始值0,別人喝水前麻煩先做個累加才能喝水。

循環時間長開銷大。

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

只能保證一個共享變量的原子操作。

當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖。

還有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如,有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象裏來進行CAS操作。

Jdk中相關原子操作類的使用

AtomicInteger

•int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果。

•boolean compareAndSet(int expect,int update):如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值。

•int getAndIncrement():以原子方式將當前值加1,注意,這裏返回的是自增前的值。

•int getAndSet(int newValue):以原子方式設置爲newValue的值,並返回舊值。

AtomicIntegerArray

主要是提供原子的方式更新數組裏的整型,其常用方法如下。

•int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加。

•boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。

需要注意的是,數組value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前數組複製一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組。

更新引用類型

原子更新基本類型的AtomicInteger,只能更新一個變量,如果要原子更新多個變量,就需要使用這個原子更新引用類型提供的類。Atomic包提供了以下3個類。

AtomicReference

原子更新引用類型。

AtomicStampedReference

利用版本戳的形式記錄了每次改變以後的版本號,這樣的話就不會存在ABA問題了。這就是AtomicStampedReference的解決方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作爲計數器使用,AtomicMarkableReference的pair使用的是boolean mark。 還是那個水的例子,AtomicStampedReference可能關心的是動過幾次,AtomicMarkableReference關心的是有沒有被人動過,方法都比較簡單。

AtomicMarkableReference:

原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。

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