在談?wù)摕o鎖概念時(shí),總會(huì)關(guān)聯(lián)起樂觀派與悲觀派,對(duì)于樂觀派而言,他們認(rèn)為事情總會(huì)往好的方向發(fā)展,總是認(rèn)為壞的情況發(fā)生的概率特別小,可以無所顧忌地做事,但對(duì)于悲觀派而言,他們總會(huì)認(rèn)為發(fā)展事態(tài)如果不及時(shí)控制,以后就無法挽回了,即使無法挽回的局面幾乎不可能發(fā)生。這兩種派系映射到并發(fā)編程中就如同加鎖與無鎖的策略,即加鎖是一種悲觀策略,無鎖是一種樂觀策略,因?yàn)閷?duì)于加鎖的并發(fā)程序來說,它們總是認(rèn)為每次訪問共享資源時(shí)總會(huì)發(fā)生沖突,因此必須對(duì)每一次數(shù)據(jù)操作實(shí)施加鎖策略。而無鎖則總是假設(shè)對(duì)共享資源的訪問沒有沖突,線程可以不停執(zhí)行,無需加鎖,無需等待,一旦發(fā)現(xiàn)沖突,無鎖策略則采用一種稱為CAS的技術(shù)來保證線程執(zhí)行的安全性,這項(xiàng)CAS技術(shù)就是無鎖策略實(shí)現(xiàn)的關(guān)鍵,下面我們進(jìn)一步了解CAS技術(shù)的奇妙之處。
創(chuàng)新互聯(lián)是一家專注于成都做網(wǎng)站、成都網(wǎng)站設(shè)計(jì)與策劃設(shè)計(jì),金臺(tái)網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)10年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:金臺(tái)等地區(qū)。金臺(tái)做網(wǎng)站價(jià)格咨詢:13518219792
CAS(Compare and Swap),即比較并替換,是用于實(shí)現(xiàn)多線程同步的原子指令。
執(zhí)行函數(shù):CAS(V,E,N)
其包含3個(gè)參數(shù)
- V表示要更新的變量
- E表示預(yù)期值
- N表示新值
假定有兩個(gè)操作A和B,如果從執(zhí)行A的線程來看,當(dāng)另一個(gè)線程執(zhí)行B時(shí),要么將B全部執(zhí)行完,要么完全不執(zhí)行B,那么A和B對(duì)彼此來說是原子的。
實(shí)現(xiàn)原子操作可以使用鎖,鎖機(jī)制,滿足基本的需求是沒有問題的了,但是有的時(shí)候我們的需求并非這么簡(jiǎn)單,我們需要更有效,更加靈活的機(jī)制,synchronized關(guān)鍵字是基于阻塞的鎖機(jī)制,也就是說當(dāng)一個(gè)線程擁有鎖的時(shí)候,訪問同一資源的其它線程需要等待,直到該線程釋放鎖,
這里會(huì)有些問題:首先,如果被阻塞的線程優(yōu)先級(jí)很高很重要怎么辦?其次,如果獲得鎖的線程一直不釋放鎖怎么辦?(這種情況是非常糟糕的)。還有一種情況,如果有大量的線程來競(jìng)爭(zhēng)資源,那CPU將會(huì)花費(fèi)大量的時(shí)間和資源來處理這些競(jìng)爭(zhēng),同時(shí),還有可能出現(xiàn)一些例如死鎖之類的情況,最后,其實(shí)鎖機(jī)制是一種比較粗糙,粒度比較大的機(jī)制,相對(duì)于像計(jì)數(shù)器這樣的需求有點(diǎn)兒過于笨重。
實(shí)現(xiàn)原子操作還可以使用當(dāng)前的處理器基本都支持CAS的指令,只不過每個(gè)廠家所實(shí)現(xiàn)的算法并不一樣,每一個(gè)CAS操作過程都包含三個(gè)運(yùn)算符:一個(gè)內(nèi)存地址V,一個(gè)期望的值A(chǔ)和一個(gè)新值B,操作的時(shí)候如果這個(gè)地址上存放的值等于這個(gè)期望的值A(chǔ),則將地址上的值賦為新值B,否則不做任何操作。
CAS的基本思路就是,如果這個(gè)地址上的值和期望的值相等,則給其賦予新值,否則不做任何事兒,但是要返回原值是多少。循環(huán)CAS就是在一個(gè)循環(huán)里不斷的做cas操作,直到成功為止。
CAS是怎么實(shí)現(xiàn)線程的安全呢?語(yǔ)言層面不做處理,我們將其交給硬件—CPU和內(nèi)存,利用CPU的多處理能力,實(shí)現(xiàn)硬件層面的阻塞,再加上volatile變量的特性即可實(shí)現(xiàn)基于原子操作的線程安全。
或許我們可能會(huì)有這樣的疑問,假設(shè)存在多個(gè)線程執(zhí)行CAS操作并且CAS的步驟很多,有沒有可能在判斷V和E相同后,正要賦值時(shí),切換了線程,更改了值。造成了數(shù)據(jù)不一致呢?答案是否定的,因?yàn)镃AS是一種系統(tǒng)原語(yǔ),原語(yǔ)屬于操作系統(tǒng)用語(yǔ)范疇,是由若干條指令組成的,用于完成某個(gè)功能的一個(gè)過程,并且原語(yǔ)的執(zhí)行必須是連續(xù)的,在執(zhí)行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會(huì)造成所謂的數(shù)據(jù)不一致問題。
說到CAS,不得不提到兩個(gè)專業(yè)詞語(yǔ):悲觀鎖,樂觀鎖。我們先來看看什么是悲觀鎖,什么是樂觀鎖。
顧名思義,就是比較悲觀的鎖,總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖(共享資源每次只給一個(gè)線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程)。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨(dú)占鎖就是悲觀鎖思想的實(shí)現(xiàn)。
反之,總是假設(shè)最好的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù),可以使用版本號(hào)機(jī)制和CAS算法實(shí)現(xiàn)。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量,像數(shù)據(jù)庫(kù)提供的類似于write_condition機(jī)制,其實(shí)都是提供的樂觀鎖。我們今天講的CAS就是樂觀鎖。
由于CAS操作屬于樂觀派,它總認(rèn)為自己可以成功完成操作,當(dāng)多個(gè)線程同時(shí)使用CAS操作一個(gè)變量時(shí),只有一個(gè)會(huì)勝出,并成功更新,其余均會(huì)失敗,但失敗的線程并不會(huì)被掛起,僅是被告知失敗,并且允許再次嘗試,當(dāng)然也允許失敗的線程放棄操作?;谶@樣的原理,CAS操作即使沒有鎖,同樣知道其他線程對(duì)共享資源操作影響,并執(zhí)行相應(yīng)的處理措施。同時(shí)從這點(diǎn)也可以看出,由于無鎖操作中沒有鎖的存在,因此不可能出現(xiàn)死鎖的情況,也就是說無鎖操作天生免疫死鎖。
非阻塞的輕量級(jí)樂觀鎖, 通過CPU指令實(shí)現(xiàn), 在資源競(jìng)爭(zhēng)不激烈的情況下性能高, 相比synchronize重量級(jí)悲觀鎖, synchronize有復(fù)雜的加鎖, 解鎖和喚醒線程操作.。
假設(shè)這樣一種場(chǎng)景,當(dāng)?shù)谝粋€(gè)線程執(zhí)行CAS(V,E,U)操作,在獲取到當(dāng)前變量V,準(zhǔn)備修改為新值U前,另外兩個(gè)線程已連續(xù)修改了兩次變量V的值,使得該值又恢復(fù)為舊值,這樣的話,我們就無法正確判斷這個(gè)變量是否已被修改過,如下圖
因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個(gè)值原來是A,變成了B,又變成了A,那么使用CAS進(jìn)行檢查時(shí)會(huì)發(fā)現(xiàn)它的值沒有發(fā)生變化,但是實(shí)際上卻變化了。
就像上圖描述的一樣,線程A原來的值是10,線程B修改為了20,但是線程C又將值修改為了10,這個(gè)時(shí)候線程A來讀取了,與舊值做判斷,發(fā)現(xiàn)還是10,沒有修改過,就做了更新操作,但是我們知道,值有過變更。
這就是典型的CAS的ABA問題,一般情況這種情況發(fā)生的概率比較小,可能發(fā)生了也不會(huì)造成什么問題,比如說我們對(duì)某個(gè)做加減法,不關(guān)心數(shù)字的過程,那么發(fā)生ABA問題也沒啥關(guān)系。但是在某些情況下還是需要防止的,那么該如何解決呢?在Java中解決ABA問題,我們可以使用以下兩個(gè)原子類。
AtomicStampedReference原子類是一個(gè)帶有時(shí)間戳的對(duì)象引用,在每次修改后,AtomicStampedReference不僅會(huì)設(shè)置新值而且還會(huì)記錄更改的時(shí)間。當(dāng)AtomicStampedReference設(shè)置對(duì)象值時(shí),對(duì)象值以及時(shí)間戳都必須滿足期望值才能寫入成功,這也就解決了反復(fù)讀寫時(shí),無法預(yù)知值是否已被修改的窘境,測(cè)試demo如下
public class ABADemo {
static AtomicInteger atIn = new AtomicInteger(100);
//初始化時(shí)需要傳入一個(gè)初始值和初始時(shí)間
static AtomicStampedReference atomicStampedR =
new AtomicStampedReference(200,0);
static Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//更新為200
atIn.compareAndSet(100, 200);
//更新為100
atIn.compareAndSet(200, 100);
}
});
static Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag=atIn.compareAndSet(100,500);
System.out.println("flag:"+flag+",newValue:"+atIn);
}
});
static Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
int time=atomicStampedR.getStamp();
//更新為200
atomicStampedR.compareAndSet(100, 200,time,time+1);
//更新為100
int time2=atomicStampedR.getStamp();
atomicStampedR.compareAndSet(200, 100,time2,time2+1);
}
});
static Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
int time = atomicStampedR.getStamp();
System.out.println("sleep 前 t4 time:"+time);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag=atomicStampedR.compareAndSet(100,500,time,time+1);
System.out.println("flag:"+flag+",newValue:"+atomicStampedR.getReference());
}
});
public static void main(String[] args) throws InterruptedException {
t1.start();
t2.start();
t1.join();
t2.join();
t3.start();
t4.start();
/**
* 輸出結(jié)果:
flag:true,newValue:500
sleep 前 t4 time:0
flag:false,newValue:200
*/
}
}
對(duì)比輸出結(jié)果可知,AtomicStampedReference類確實(shí)解決了ABA的問題,下面我們簡(jiǎn)單看看其內(nèi)部實(shí)現(xiàn)原理
public class AtomicStampedReference {
//通過Pair內(nèi)部類存儲(chǔ)數(shù)據(jù)和時(shí)間戳
private static class Pair {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
//存儲(chǔ)數(shù)值和時(shí)間的內(nèi)部類
private volatile Pair pair;
//構(gòu)造器,創(chuàng)建時(shí)需傳入初始值和時(shí)間初始值
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
}
接著看看其compareAndSet方法的實(shí)現(xiàn):
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
同時(shí)對(duì)當(dāng)前數(shù)據(jù)和當(dāng)前時(shí)間進(jìn)行比較,只有兩者都相等是才會(huì)執(zhí)行casPair()方法,單從該方法的名稱就可知是一個(gè)CAS方法,最終調(diào)用的還是Unsafe類中的compareAndSwapObject方法
private boolean casPair(Pair cmp, Pair val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
到這我們就很清晰AtomicStampedReference的內(nèi)部實(shí)現(xiàn)思想了,通過一個(gè)鍵值對(duì)Pair存儲(chǔ)數(shù)據(jù)和時(shí)間戳,在更新時(shí)對(duì)數(shù)據(jù)和時(shí)間戳進(jìn)行比較,只有兩者都符合預(yù)期才會(huì)調(diào)用Unsafe的compareAndSwapObject方法執(zhí)行數(shù)值和時(shí)間戳替換,也就避免了ABA的問題。
AtomicMarkableReference與AtomicStampedReference不同的是,AtomicMarkableReference維護(hù)的是一個(gè)boolean值的標(biāo)識(shí),也就是說至于true和false兩種切換狀態(tài),經(jīng)過測(cè)試,這種方式并不能完全防止ABA問題的發(fā)生,只能減少ABA問題發(fā)生的概率。
public class ABADemo {
static AtomicMarkableReference atMarkRef =
new AtomicMarkableReference(100,false);
static Thread t5 = new Thread(new Runnable() {
@Override
public void run() {
boolean mark=atMarkRef.isMarked();
System.out.println("mark:"+mark);
//更新為200
System.out.println("t5 result:"+atMarkRef.compareAndSet(atMarkRef.getReference(), 200,mark,!mark));
}
});
static Thread t6 = new Thread(new Runnable() {
@Override
public void run() {
boolean mark2=atMarkRef.isMarked();
System.out.println("mark2:"+mark2);
System.out.println("t6 result:"+atMarkRef.compareAndSet(atMarkRef.getReference(), 100,mark2,!mark2));
}
});
static Thread t7 = new Thread(new Runnable() {
@Override
public void run() {
boolean mark=atMarkRef.isMarked();
System.out.println("sleep 前 t7 mark:"+mark);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag=atMarkRef.compareAndSet(100,500,mark,!mark);
System.out.println("flag:"+flag+",newValue:"+atMarkRef.getReference());
}
});
public static void main(String[] args) throws InterruptedException {
t5.start();t5.join();
t6.start();t6.join();
t7.start();
/**
* 輸出結(jié)果:
mark:false
t5 result:true
mark2:true
t6 result:true
sleep 前 t5 mark:false
flag:true,newValue:500 ---->成功了.....說明還是發(fā)生ABA問題
*/
}
}
AtomicMarkableReference的實(shí)現(xiàn)原理與AtomicStampedReference類似。到此,我們也明白了如果要完全杜絕ABA問題的發(fā)生,我們應(yīng)該使用AtomicStampedReference原子類更新對(duì)象,而對(duì)于AtomicMarkableReference來說只能減少ABA問題的發(fā)生概率,并不能杜絕。
自旋CAS如果長(zhǎng)時(shí)間不成功,會(huì)給CPU帶來非常大的執(zhí)行開銷。
當(dāng)對(duì)一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來保證原子操作,但是對(duì)多個(gè)共享變量操作時(shí),循環(huán)CAS就無法保證操作的原子性,這個(gè)時(shí)候就可以用鎖。
還有一個(gè)取巧的辦法,就是把多個(gè)共享變量合并成一個(gè)共享變量來操作。比如,有兩個(gè)共享變量i=2,j=a,合并一下ij=2a,然后用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對(duì)象之間的原子性,就可以把多個(gè)變量放在一個(gè)對(duì)象里來進(jìn)行CAS操作。
本文由
傳智教育博學(xué)谷
教研團(tuán)隊(duì)發(fā)布。如果本文對(duì)您有幫助,歡迎
關(guān)注
和點(diǎn)贊
;如果您有任何建議也可留言評(píng)論
或私信
,您的支持是我堅(jiān)持創(chuàng)作的動(dòng)力。轉(zhuǎn)載請(qǐng)注明出處!