您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關(guān)Java CAS及其應(yīng)用之自旋鎖 Atomic類的示例分析,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
一、CAS 操作
樂觀鎖用到的機(jī)制就是CAS,Compare and Swap。
CAS有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。
1、非阻塞算法 (nonblocking algorithms)
一個(gè)線程的失敗或者掛起不應(yīng)該影響其他線程的失敗或掛起的算法。
現(xiàn)代的CPU提供了特殊的指令,可以自動(dòng)更新共享數(shù)據(jù),而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。
2、Atomic示例
Atomic包是java.util.concurrent下的另一個(gè)專門為線程安全設(shè)計(jì)的Java包,包含多個(gè)原子操作類。這個(gè)包里面提供了一組原子變量類。其基本的特性就是在多線程環(huán)境下,當(dāng)有多個(gè)線程同時(shí)執(zhí)行這些類的實(shí)例包含的方法時(shí),具有排他性,即當(dāng)某個(gè)線程進(jìn)入方法,執(zhí)行其中的指令時(shí),不會(huì)被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執(zhí)行完成,才由JVM從等待隊(duì)列中選擇一個(gè)另一個(gè)線程進(jìn)入,這只是一種邏輯上的理解。實(shí)際上是借助硬件的相關(guān)指令來實(shí)現(xiàn)的,不會(huì)阻塞線程(或者說只是在硬件級(jí)別上阻塞了)??梢詫?duì)基本數(shù)據(jù)、數(shù)組中的基本數(shù)據(jù)、對(duì)類中的基本數(shù)據(jù)進(jìn)行操作。原子變量類相當(dāng)于一種泛化的volatile變量,能夠支持原子的和有條件的讀-改-寫操作。
AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference這四種基本類型用來處理布爾,整數(shù),長整數(shù),對(duì)象四種數(shù)據(jù),其內(nèi)部實(shí)現(xiàn)不是簡單的使用synchronized,而是一個(gè)更為高效的方式CAS (compare and swap) + volatile和native方法,從而避免了synchronized的高開銷,執(zhí)行效率大為提升。我們來看個(gè)例子,與我們平時(shí)i++所對(duì)應(yīng)的原子操作為:getAndIncrement()
public static void main(String[] args) { AtomicInteger ai = new AtomicInteger(); System.out.println(ai); ai.getAndIncrement(); System.out.println(ai); }
拿出AtomicInteger來研究在沒有鎖的情況下是如何做到數(shù)據(jù)正確性的。
private volatile int value;
在沒有鎖的機(jī)制下需要借助volatile原語,保證線程間的數(shù)據(jù)是可見的(共享的)。
這樣才獲取變量的值的時(shí)候才能直接讀取。
public final int get() { return value; }
然后來看看 ++i 是怎么做到的。
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
在這里采用了CAS操作,每次從內(nèi)存中讀取數(shù)據(jù)然后將此數(shù)據(jù)和+1后的結(jié)果進(jìn)行CAS操作,如果成功就返回結(jié)果,否則重試直到成功為止。
而compareAndSet利用JNI來完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
整體的過程就是這樣子的,利用CPU的CAS指令,同時(shí)借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。
而整個(gè)J.U.C都是建立在CAS之上的,因此對(duì)于synchronized阻塞算法,J.U.C在性能上有了很大的提升。參考資料的文章中介紹了如果利用CAS構(gòu)建非阻塞計(jì)數(shù)器、隊(duì)列等數(shù)據(jù)結(jié)構(gòu)。
二、ABA問題
CAS看起來很爽,但是會(huì)導(dǎo)致“ABA問題”。
CAS算法實(shí)現(xiàn)一個(gè)重要前提需要取出內(nèi)存中某時(shí)刻的數(shù)據(jù),而在下時(shí)刻比較并替換,那么在這個(gè)時(shí)間差類會(huì)導(dǎo)致數(shù)據(jù)的變化。
比如說一個(gè)線程one從內(nèi)存位置V中取出A,這時(shí)候另一個(gè)線程two也從內(nèi)存中取出A,并且two進(jìn)行了一些操作變成了B,然后two又將V位置的數(shù)據(jù)變成A,這時(shí)候線程one進(jìn)行CAS操作發(fā)現(xiàn)內(nèi)存中仍然是A,然后one操作成功。盡管線程one的CAS操作成功,但是不代表這個(gè)過程就是沒有問題的。如果鏈表的頭在變化了兩次后恢復(fù)了原值,但是不代表鏈表就沒有變化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。這允許一對(duì)變化的元素進(jìn)行原子操作。
在運(yùn)用CAS做Lock-Free操作中有一個(gè)經(jīng)典的ABA問題:
線程1準(zhǔn)備用CAS將變量的值由A替換為B,在此之前,線程2將變量的值由A替換為C,又由C替換為A,然后線程1執(zhí)行CAS時(shí)發(fā)現(xiàn)變量的值仍然為A,所以CAS成功。但實(shí)際上這時(shí)的現(xiàn)場已經(jīng)和最初不同了,盡管CAS成功,但可能存在潛藏的問題,例如下面的例子:現(xiàn)有一個(gè)用單向鏈表實(shí)現(xiàn)的堆棧,棧頂為A,這時(shí)線程T1已經(jīng)知道A.next為B,然后希望用CAS將棧頂替換為B:
head.compareAndSet(A,B);
在T1執(zhí)行上面這條指令之前,線程T2介入,將A、B出棧,再pushD、C、A,此時(shí)堆棧結(jié)構(gòu)如下圖,而對(duì)象B此時(shí)處于游離狀態(tài):
此時(shí)輪到線程T1執(zhí)行CAS操作,檢測發(fā)現(xiàn)棧頂仍為A,所以CAS成功,棧頂變?yōu)锽,但實(shí)際上B.next為null,所以此時(shí)的情況變?yōu)椋?/p>
其中堆棧中只有B一個(gè)元素,C和D組成的鏈表不再存在于堆棧中,平白無故就把C、D丟掉了。
以上就是由于ABA問題帶來的隱患,各種樂觀鎖的實(shí)現(xiàn)中通常都會(huì)用版本戳version來對(duì)記錄或?qū)ο髽?biāo)記,避免并發(fā)操作帶來的問題,在Java中,AtomicStampedReference<E>也實(shí)現(xiàn)了這個(gè)作用,它通過包裝[E,Integer]的元組來對(duì)對(duì)象標(biāo)記版本戳stamp,從而避免ABA問題,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對(duì)初始值為100的原子整型變量進(jìn)行更新,AtomicInteger會(huì)成功執(zhí)行CAS操作,而加上版本戳的AtomicStampedReference對(duì)于ABA問題會(huì)執(zhí)行CAS失?。?/p>
package concur.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; public class ABA { private static AtomicInteger atomicInt = new AtomicInteger(100); private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0); public static void main(String[] args) throws InterruptedException { Thread intT1 = new Thread(new Runnable() { @Override public void run() { atomicInt.compareAndSet(100, 101); atomicInt.compareAndSet(101, 100); } }); Thread intT2 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } boolean c3 = atomicInt.compareAndSet(100, 101); System.out.println(c3); //true } }); intT1.start(); intT2.start(); intT1.join(); intT2.join(); Thread refT1 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1); atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1); } }); Thread refT2 = new Thread(new Runnable() { @Override public void run() { int stamp = atomicStampedRef.getStamp(); System.out.println("before sleep : stamp = " + stamp); // stamp = 0 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1 boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1); System.out.println(c3); //false } }); refT1.start(); refT2.start(); } }
三.自旋鎖詳解
自旋鎖的概念
首先是一種鎖,與互斥鎖相似,基本作用是用于線程(進(jìn)程)之間的同步。與普通鎖不同的是,一個(gè)線程A在獲得普通鎖后,如果再有線程B試圖獲取鎖,那么這個(gè)線程B將會(huì)掛起(阻塞);試想下,如果兩個(gè)線程資源競爭不是特別激烈,而處理器阻塞一個(gè)線程引起的線程上下文的切換的代價(jià)高于等待資源的代價(jià)的時(shí)候(鎖的已保持者保持鎖時(shí)間比較短),那么線程B可以不放棄CPU時(shí)間片,而是在“原地”忙等,直到鎖的持有者釋放了該鎖,這就是自旋鎖的原理,可見自旋鎖是一種非阻塞鎖。
自旋鎖可能引起的問題:
1.過多占據(jù)CPU時(shí)間:如果鎖的當(dāng)前持有者長時(shí)間不釋放該鎖,那么等待者將長時(shí)間的占據(jù)cpu時(shí)間片,導(dǎo)致CPU資源的浪費(fèi),因此可以設(shè)定一個(gè)時(shí)間,當(dāng)鎖持有者超過這個(gè)時(shí)間不釋放鎖時(shí),等待者會(huì)放棄CPU時(shí)間片阻塞;
2.死鎖問題:試想一下,有一個(gè)線程連續(xù)兩次試圖獲得自旋鎖(比如在遞歸程序中),第一次這個(gè)線程獲得了該鎖,當(dāng)?shù)诙卧噲D加鎖的時(shí)候,檢測到鎖已被占用(其實(shí)是被自己占用),那么這時(shí),線程會(huì)一直等待自己釋放該鎖,而不能繼續(xù)執(zhí)行,這樣就引起了死鎖。因此遞歸程序使用自旋鎖應(yīng)該遵循以下原則:遞歸程序決不能在持有自旋鎖時(shí)調(diào)用它自己,也決不能在遞歸調(diào)用時(shí)試圖獲得相同的自旋鎖。
class SpinLock { //java中原子(CAS)操作 AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋鎖的線程對(duì)象 private int count; public void lock() { Thread cur = Thread.currentThread(); //lock函數(shù)將owner設(shè)置為當(dāng)前線程,并且預(yù)測原來的值為空。unlock函數(shù)將owner設(shè)置為null,并且預(yù)測值為當(dāng)前線程。 //當(dāng)有第二個(gè)線程調(diào)用lock操作時(shí)由于owner值不為空,導(dǎo)致循環(huán) //一直被執(zhí)行,直至第一個(gè)線程調(diào)用unlock函數(shù)將owner設(shè)置為null,第二個(gè)線程才能進(jìn)入臨界區(qū)。 while (!owner.compareAndSet(null, cur)){ } } public void unLock() { Thread cur = Thread.currentThread(); owner.compareAndSet(cur, null); } } }
看完上述內(nèi)容,你們對(duì)Java CAS及其應(yīng)用之自旋鎖 Atomic類的示例分析有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。