對(duì)于并發(fā)控制而言,我們平時(shí)用的鎖(synchronized,,Lock)是一種悲觀的策略,。它總是假設(shè)每一次臨界區(qū)操作會(huì)產(chǎn)生沖突,因此,,必須對(duì)每次操作都小心翼翼,。如果多個(gè)線程同時(shí)訪問臨界區(qū)資源,就寧可犧牲性能讓線程進(jìn)行等待,,所以鎖會(huì)阻塞線程執(zhí)行,。 與之相對(duì)的有一種樂觀的策略,它會(huì)假設(shè)對(duì)資源的訪問是沒有沖突的,。既然沒有沖突也就無需等待了,,所有的線程都在不停頓的狀態(tài)下持續(xù)執(zhí)行。那如果遇到問題了無鎖的策略使用一種叫做比較交換(CAS Compare And Swap)來鑒別線程沖突,,一旦檢測(cè)到?jīng)_突產(chǎn)生,,就重試當(dāng)前操作直到?jīng)]有沖突。CAS算法是非阻塞的,,它對(duì)死鎖問題天生免疫,,而且它比基于鎖的方式擁有更優(yōu)越的性能。 CAS算法的過程是這樣:它包含三個(gè)參數(shù) CAS(V,E,N),。V表示要更新的變量,,E表示預(yù)期的值,N表示新值,。僅當(dāng)V值等于E值時(shí),,才會(huì)將V的值設(shè)置成N,否則什么都不做,。最后CAS返回當(dāng)前V的值,。CAS算法需要你額外給出一個(gè)期望值,也就是你認(rèn)為現(xiàn)在變量應(yīng)該是什么樣子,,如果變量不是你想象的那樣,,那說明已經(jīng)被別人修改過,。你就重新讀取,再次嘗試修改即可,。 JDK并發(fā)包有一個(gè)atomic包,,里面實(shí)現(xiàn)了一些直接使用CAS操作的線程安全的類型。其中最常用的一個(gè)類應(yīng)該就是AtomicInteger,。我們以此為例來研究一下沒有鎖的情況下如何做到線程安全,。 private volatile int value;
這是AtomicInteger類的核心字段,,代表當(dāng)前實(shí)際取值,,借助volatile保證線程間數(shù)據(jù)的可見性,。 獲取內(nèi)部數(shù)據(jù)的方法: public final int get() { return value; } 我們關(guān)注一下incrementAndGet()的內(nèi)部實(shí)現(xiàn) 1 public final int incrementAndGet() { 2 for (;;) { 3 int current = get(); 4 int next = current + 1; 5 if (compareAndSet(current, next)) 6 return next; 7 } 8 }
代碼第二行使用了一個(gè)死循環(huán),,原因是:CAS的操作未必都是成功的,因此對(duì)于不成功的情況,,我們就需要進(jìn)行不斷的嘗試,。第三行取得當(dāng)前值,,接著+1得到新值next,。這里我們使用CAS必需的兩個(gè)參數(shù):期望值以及新值,。使用compareAndSet()將新值next寫入,。成功的條件是在寫入的時(shí)刻,,當(dāng)前的值應(yīng)該要等于剛剛?cè)〉降腸urrent。如果不是這樣則說明AtomicInteger的值在第3行到第5行之間被其他線程修改過了,。當(dāng)前看到的狀態(tài)是一個(gè)過期的狀態(tài),,因此返回失敗,需要進(jìn)行下一次重試,,知道成功為止,。 public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } 整體的過程就是這樣子的,利用CPU的CAS指令,,同時(shí)借助JNI來完成Java的非阻塞算法,。其它原子操作都是利用類似的特性完成的。大概的邏輯應(yīng)該是這樣: if (this == expect) { this = update return true; } else { return false; } CAS雖然能高效的解決原子問題,,但是CAS也會(huì)帶來1個(gè)經(jīng)典問題即ABA問題: 因?yàn)镃AS需要在操作值的時(shí)候檢查下值有沒有發(fā)生變化,,如果沒有發(fā)生變化則更新,但是如果一個(gè)值原來是A,,變成了B,,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒有發(fā)生變化,,但是實(shí)際上卻變化了,。 ABA問題的解決思路就是使用版本號(hào)。在變量前面追加上版本號(hào),,每次變量更新的時(shí)候把版本號(hào)加一,,那么A-B-A 就會(huì)變成1A-2B-3A。 從Java1.5開始JDK的atomic包里提供了一個(gè)類AtomicStampedReference來解決ABA問題。這個(gè)類在內(nèi)部不僅維護(hù)了對(duì)象值,,還維護(hù)了一個(gè)時(shí)間戳(可以是任意的一個(gè)整數(shù)來表示狀態(tài)值),。當(dāng)設(shè)置對(duì)象值時(shí),對(duì)象值和狀態(tài)值都必須滿足期望值才會(huì)寫入成功,。因此即使對(duì)象被反復(fù)讀寫,,寫會(huì)原值,只要狀態(tài)值發(fā)生變化,,就能防止不恰當(dāng)?shù)膶懭?。 ?/p> /**
|
|