Java多線程之并發(fā)編程的基石CAS機(jī)制詳解
前言: synchronized保證了線程安全,但是在某些情況下,卻不是一個最優(yōu)選擇,關(guān)鍵在于性能問題。Java中提供了很多原子操作類來保證共享變量操作的原子性。這些原子操作的底層原理都是使用了CAS機(jī)制。既然用鎖或 synchronized 關(guān)鍵字可以實(shí)現(xiàn)原子操作,那么為什么還要用 CAS 呢,因?yàn)榧渔i或使用 synchronized 關(guān)鍵字帶來的性能損耗較大,而用 CAS 可以實(shí)現(xiàn)樂觀鎖,它實(shí)際上是直接利用了 CPU 層面的指令,沒有加鎖和線程上下文切換的開銷,所以性能很高。
一、CAS機(jī)制簡介
1.1、悲觀鎖和樂觀鎖更新數(shù)據(jù)方式
CAS機(jī)制是一種數(shù)據(jù)更新的方式。在具體講什么是CAS機(jī)制之前,我們先來聊下在多線程環(huán)境下,對共享變量進(jìn)行數(shù)據(jù)更新的兩種模式:悲觀鎖模式和樂觀鎖模式。
悲觀鎖更新的方式認(rèn)為:在更新數(shù)據(jù)的時候大概率會有其他線程去爭奪共享資源,所以悲觀鎖的做法是:第一個獲取資源的線程會將資源鎖定起來,其他沒爭奪到資源的線程只能進(jìn)入阻塞隊(duì)列,等第一個獲取資源的線程釋放鎖之后,這些線程才能有機(jī)會重新爭奪資源。synchronized就是Java中悲觀鎖的典型實(shí)現(xiàn),synchronized使用起來非常簡單方便,但是會使沒爭搶到資源的線程進(jìn)入阻塞狀態(tài),線程在阻塞狀態(tài)和Runnable狀態(tài)之間切換效率較低(比較慢)。比如你的更新操作其實(shí)是非常快的,這種情況下你還用synchronized將其他線程都鎖住了,線程從Blocked狀態(tài)切換回Runnable華的時間可能比你的更新操作的時間還要長。
樂觀鎖更新方式認(rèn)為:在更新數(shù)據(jù)的時候其他線程爭搶這個共享變量的概率非常小,所以更新數(shù)據(jù)的時候不會對共享數(shù)據(jù)加鎖。但是在正式更新數(shù)據(jù)之前會檢查數(shù)據(jù)是否被其他線程改變過,如果未被其他線程改變過就將共享變量更新成最新值,如果發(fā)現(xiàn)共享變量已經(jīng)被其他線程更新過了,就重試,直到成功為止。CAS機(jī)制就是樂觀鎖的典型實(shí)現(xiàn)。
1.2、什么是CAS機(jī)制
CAS,是Compare and Swap的簡稱,是一種用于在多線程環(huán)境下實(shí)現(xiàn)同步功能的機(jī)制。CAS 操作包含三個操作數(shù) -- 內(nèi)存位置、預(yù)期數(shù)值和新值。CAS 的實(shí)現(xiàn)邏輯是將內(nèi)存位置處的數(shù)值與預(yù)期數(shù)值相比較,若相等,則將內(nèi)存位置處的值替換為新值。若不相等,則不做任何操作。
在 Java 中,Java 并沒有直接實(shí)現(xiàn) CAS,CAS 相關(guān)的實(shí)現(xiàn)是通過 C++ 內(nèi)聯(lián)匯編的形式實(shí)現(xiàn)的。Java 代碼需通過 JNI 才能調(diào)用。
CAS這個機(jī)制中有三個核心的參數(shù):
主內(nèi)存中存放的共享變量的值:V(一般情況下這個V是內(nèi)存的地址值,通過這個地址可以獲得內(nèi)存中的值)
工作內(nèi)存中共享變量的副本值,也叫預(yù)期值:A
需要將共享變量更新到的最新值:B
如上圖中,主存中保存V值,線程中要使用V值要先從主存中讀取V值到線程的工作內(nèi)存A中,然后計算后變成B值,最后再把B值寫回到內(nèi)存V值中。多個線程共用V值都是如此操作。CAS的核心是在將B值寫入到V之前要比較A值和V值是否相同,如果不相同證明此時V值已經(jīng)被其他線程改變,重新將V值賦給A,并重新計算得到B,如果相同,則將B值賦給V。
值得注意的是CAS機(jī)制中的這步步驟是原子性的(從指令層面提供的原子操作),所以CAS機(jī)制可以解決多線程并發(fā)編程對共享變量讀寫的原子性問題。
1.3、CAS與sychronized比較
從思想上來說:
①. synchronized屬于【悲觀鎖】
悲觀鎖認(rèn)為:程序中的【并發(fā)】情況嚴(yán)重,所以【嚴(yán)防死守】
②. CAS屬于【樂觀鎖】
樂觀鎖認(rèn)為:程序中的【并發(fā)】情況不那么嚴(yán)重,所以讓【線程不斷去嘗試更新】
這2種機(jī)制沒有絕對的好與壞,關(guān)鍵看使用場景。在并發(fā)量非常高的情況下,反而用同步鎖更合適一些。
1.4、Java中都有哪些地方應(yīng)用到了CAS機(jī)制呢?
a、Atomic系列類
b、Lock系列類底層實(shí)現(xiàn)
c、Java1.6以上版本,synchronized轉(zhuǎn)變?yōu)橹亓考夋i之前,也會采用CAS機(jī)制
1.5、CAS 實(shí)現(xiàn)自旋鎖
既然用鎖或 synchronized 關(guān)鍵字可以實(shí)現(xiàn)原子操作,那么為什么還要用 CAS 呢,因?yàn)榧渔i或使用 synchronized 關(guān)鍵字帶來的性能損耗較大,而用 CAS 可以實(shí)現(xiàn)樂觀鎖,它實(shí)際上是直接利用了 CPU 層面的指令,沒有加鎖和線程上下文切換的開銷,所以性能很高。
上面也說了,CAS 是實(shí)現(xiàn)自旋鎖的基礎(chǔ),CAS 利用 CPU 指令保證了操作的原子性,以達(dá)到鎖的效果,至于自旋呢,看字面意思也很明白,自己旋轉(zhuǎn),翻譯成人話就是循環(huán),一般是用一個無限循環(huán)實(shí)現(xiàn)。這樣一來,一個無限循環(huán)中,執(zhí)行一個 CAS 操作,當(dāng)操作成功,返回 true 時,循環(huán)結(jié)束;當(dāng)返回 false 時,接著執(zhí)行循環(huán),繼續(xù)嘗試 CAS 操作,直到返回 true。
其實(shí) JDK 中有好多地方用到了 CAS ,尤其是 java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic 包下,相信大家都用到過 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。
1.6、CAS機(jī)制優(yōu)缺點(diǎn)
CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環(huán)時間長開銷大和只能保證一個共享變量的原子操作
CAS機(jī)制CAS機(jī)制缺點(diǎn)
1>ABA問題
ABA問題:CAS在操作的時候會檢查變量的值是否被更改過,如果沒有則更新值,但是帶來一個問題,最開始的值是A,接著變成B,最后又變成了A。經(jīng)過檢查這個值確實(shí)沒有修改過,因?yàn)樽詈蟮闹颠€是A,但是實(shí)際上這個值確實(shí)已經(jīng)被修改過了。為了解決這個問題,在每次進(jìn)行操作的時候加上一個版本號,每次操作的就是兩個值,一個版本號和某個值,A——>B——>A問題就變成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference類解決ABA問題,用Pair這個內(nèi)部類實(shí)現(xiàn),包含兩個屬性,分別代表版本號和引用,在compareAndSet中先對當(dāng)前引用進(jìn)行檢查,再對版本號標(biāo)志進(jìn)行檢查,只有全部相等才更新值。
AtomicStampedReference
和AtomicMarkableReference
就是用來解決CAS中的ABA問題的。他們解決ABA問題的原理類似,都是通過一個版本號來區(qū)分有沒被更新過。
AtomicStampedReference
:帶版本戳的原子引用類型,版本戳為int類型。
AtomicMarkableReference
:帶版本戳的原子引用類型,版本戳為boolean類型。
2>可能會消耗較高的CPU
看起來CAS比鎖的效率高,從阻塞機(jī)制變成了非阻塞機(jī)制,減少了線程之間等待的時間。每個方法不能絕對的比另一個好,在線程之間競爭程度大的時候,如果使用CAS,每次都有很多的線程在競爭,也就是說CAS機(jī)制不能更新成功。這種情況下CAS機(jī)制會一直重試,這樣就會比較耗費(fèi)CPU。因此可以看出,如果線程之間競爭程度小,使用CAS是一個很好的選擇;但是如果競爭很大,使用鎖可能是個更好的選擇。在并發(fā)量非常高的環(huán)境中,如果仍然想通過原子類來更新的話,可以使用AtomicLong的替代類:LongAdder。
3>不能保證代碼塊的原子性
Java中的CAS機(jī)制只能保證一個共享變量的原子操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進(jìn)行原子性的更新,就不得不使用Synchronized了。
CAS機(jī)制優(yōu)點(diǎn)
可以保證變量操作的原子性;
并發(fā)量不是很高的情況下,使用CAS機(jī)制比使用鎖機(jī)制效率更高;
在線程對共享資源占用時間較短的情況下,使用CAS機(jī)制效率也會較高。
二、Java提供的CAS操作類--Unsafe類
2.1、Unsafe類簡介
在研究JDK中AQS時,會發(fā)現(xiàn)這個類很多地方都使用了CAS操作,在并發(fā)實(shí)現(xiàn)中CAS操作必須具備原子性,而且是硬件級別的原子性,Java被隔離在硬件之上,明顯力不從心,這時為了能直接操作操作系統(tǒng)層面,肯定要通過用C++編寫的native本地方法來擴(kuò)展實(shí)現(xiàn)。JDK提供了一個類來滿足CAS的要求,sun.misc.Unsafe,從名字上可以大概知道它用于執(zhí)行低級別、不安全的操作,AQS就是使用此類完成硬件級別的原子操作。UnSafe通過JNI調(diào)用本地C++代碼,C++代碼調(diào)用CPU硬件指令集。
Unsafe是一個很強(qiáng)大的類,它可以分配內(nèi)存、釋放內(nèi)存、可以定位對象某字段的位置、可以修改對象的字段值、可以使線程掛起、使線程恢復(fù)、可進(jìn)行硬件級別原子的CAS操作等等。
從Java5開始引入了對CAS機(jī)制的底層的支持,在這之前需要開發(fā)人員編寫相關(guān)的代碼才可以實(shí)現(xiàn)CAS。在原子變量類Atomic中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代碼,在這里的代碼都是調(diào)用了底層(核心代碼調(diào)用native修飾的方法)的實(shí)現(xiàn)方法。
在AtomicInteger源碼中可以看getAndSet方法和compareAndSet方法之間的關(guān)系,compareAndSet方法調(diào)用了底層的實(shí)現(xiàn),該方法可以實(shí)現(xiàn)與一個volatile變量的讀取和寫入相同的效果。在前面說到了volatile不支持例如i++這樣的復(fù)合操作,在Atomic中提供了實(shí)現(xiàn)該操作的方法。JVM對CAS的支持通過這些原子類(Atomic***)暴露出來,供我們使用。
而Atomic系類的類底層調(diào)用的是Unsafe類的API,Unsafe類提供了一系列的compareAndSwap*方法,下面就簡單介紹下Unsafe類的API:
long objectFieldOffset(Field field)方法:返回指定的變量在所屬類中的內(nèi)存偏移地址,該偏移地址僅僅在該Unsafe函數(shù)中訪問指定字段時使用。如下代碼使用Unsafe類獲取變量value在AtomicLong對象中的內(nèi)存偏移。
static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
int arrayBaseOffset(Class arrayClass)
方法:獲取數(shù)組中第一個元素的地址。int arrayIndexScale(Class arrayClass)
方法:獲取數(shù)組中一個元素占用的字節(jié)。boolean compareAndSwapLong(Object obj, long offset, long expect, long update)
方法:比較對象obj中偏移量為offset的變量的值是否與expect相等,相等則使用update值更新,然后返回true,否則返回false,這次處理器提供的一個原子性指令。public native long getLongvolatile(Object obj, long offset)
方法:獲取對象obj中偏移量為offset的變量對應(yīng)volatile語義的值。void putLongvolatile(Object obj, long offset, long value)
方法:設(shè)置obj對象中offset偏移的類型為long的field的值為value,支持volatile語義。void putOrderedLong(Object obj, long offset, long value)
方法:設(shè)置obj對象中offset偏移地址對應(yīng)的long型field的值為value。這是一個有延遲的putLongvolatile方法,并且不保證值修改對其他線程立刻可見。只有在變量使用volatile修飾并且預(yù)計會被意外修改時才使用該方法。void park(boolean isAbsolute, long time)
方法:阻塞當(dāng)前線程,其中參數(shù)isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞線程會被喚醒,這個time是個相對值,是個增量值,也就是相對當(dāng)前時間累加time后當(dāng)前線程就會被喚醒。如果isAbsolute等于true,并且time大于0,則表示阻塞的線程到指定的時間點(diǎn)后會被喚醒,這里time是個絕對時間,是將某個時間點(diǎn)換算為ms后的值。另外,當(dāng)其他線程調(diào)用了當(dāng)前阻塞線程的interrupt方法而中斷了當(dāng)前線程時,當(dāng)前線程也會返回,而當(dāng)其他線程調(diào)用了unPark方法并且把當(dāng)前線程作為參數(shù)時當(dāng)前線程也會返回。void unpark(Object thread)
方法:喚醒調(diào)用park后阻塞的線程。
下面是JDK8新增的函數(shù),這里只列出Long類型操作。
long getAndSetLong(Object obj, long offset, long update)
方法:獲取對象obj中偏移量為offset的變量volatile語義的當(dāng)前值,并設(shè)置變量volatile語義的值為update。
//這個方法只是封裝了compareAndSwapLong的使用,不需要自己寫重試機(jī)制 public final long getAndSetLong(Object var1, long var2, long var4) { long var6; do { var6 = this.getLongVolatile(var1, var2); } while(!this.compareAndSwapLong(var1, var2, var6, var4)); return var6; }
long getAndAddLong(Object obj, long offset, long addValue)方法:獲取對象obj中偏移量為offset的變量volatile語義的當(dāng)前值,并設(shè)置變量值為原始值+addValue,原理和上面的方法類似。
2.2、Unsafe類的使用
三、CAS使用場景
- 使用一個變量統(tǒng)計網(wǎng)站的訪問量;
- Atomic類操作;
- 數(shù)據(jù)庫樂觀鎖更新。
3.1、使用一個變量統(tǒng)計網(wǎng)站的訪問量
要實(shí)現(xiàn)一個網(wǎng)站訪問量的計數(shù)器,可以通過一個Long類型的對象,并加上synchronized內(nèi)置鎖的方式。但是這種方式使得多線程的訪問變成了串行的,同一時刻只能有一個線程可以更改long的值,那么為了能夠使多線程并發(fā)的更新long的值,我們可以使用J.U.C包中的Atomic原子類。這些類的更新是原子的,不需要加鎖即可實(shí)現(xiàn)并發(fā)的更新,并且是線程安全的。
可是Atomic原子類是怎么保證并發(fā)更新的線程安全的呢?讓我們看一下AtomicLong的自增方法incrementAndGet():
public final long incrementAndGet() { // 無限循環(huán),即自旋 for (;;) { // 獲取主內(nèi)存中的最新值 long current = get(); long next = current + 1; // 通過CAS原子更新,若能成功則返回,否則繼續(xù)自旋 if (compareAndSet(current, next)) return next; } } private volatile long value; public final long get() { return value; }
可以發(fā)現(xiàn)其內(nèi)部保持著一個volatile修飾的long變量,volatile保證了long的值更新后,其他線程能立即獲得最新的值。
在incrementAndGet中首先是一個無限循環(huán)(自旋),然后獲取long的最新值,將long加1,然后通過compareAndSet()方法嘗試將long的值有current更新為next。如果能更新成功,則說明當(dāng)前還沒有其他線程更新該值,則返回next,如果更新失敗,則說明有其他線程提前更新了該值,則當(dāng)前線程繼續(xù)自旋嘗試更新。
簡單總結(jié)
總體來說,AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference原理比較簡單:使用CAS保證原子性,使用volatile保證可見性,最終能保證共享變量操作的線程安全。
AtomicLongArray、AtomicIntArray和AtomicReferenceArray的實(shí)現(xiàn)原理略有不同,是用CAS機(jī)制配合final機(jī)制來實(shí)現(xiàn)共享變量操作的線程安全的。感興趣的同學(xué)可以自己分析下,也是比較簡單的。
CAS的操作其底層是通過調(diào)用sun.misc.Unsafe類中的CompareAndSwap的方法保證線程安全的。Unsafe類中主要有下面三種CompareAndSwap方法:
public final native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update); public final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update); public final native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);
可以看到這些方法都是native的,需要調(diào)用JNI接口,也即通過操作系統(tǒng)來保證這些方法的執(zhí)行。
3.2、現(xiàn)在我們嘗試在代碼中引入AtomicInteger類
在使用Integer的時候,必須加上synchronized保證不會出現(xiàn)并發(fā)線程同時訪問的情況
public class AtomicInteger { private static Integer count =0; public static void main(String[] args) { //開啟兩個線程 for(int i=0;i<2;i++) { new Thread(new Runnable() { @Override public void run() { //每個線程當(dāng)中讓count自增1000次 for(int j=0;j<1000;j++) { increment(); } } }).start(); } //讓主線程睡2秒,避免直接打印count值為0 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count="+count); } //加上synchronized保證不會出現(xiàn)并發(fā)線程同時訪問的情況,否則結(jié)果可能只有1千多 public synchronized static void increment() { count++; } }
而在AtomicInteger中卻不用加上synchronized,在這里AtomicInteger是提供原子操作的
在某些情況下,原子類代碼的性能會比Synchronized更好,因?yàn)闆]有加鎖的線程同步上下文切換開銷,底層采用了CAS機(jī)制保證共享變量原子性,還配合volatile保證內(nèi)存可見性,最終能保證共享變量操作的線程安全。
四、Java中的原子操作類
在JDK1.5版本之前,多行代碼的原子性主要通過synchronized關(guān)鍵字進(jìn)行保證。在JDK1.5版本,Java提供了原子類型專門確保變量操作的原子性。所謂的原子操作類,指的是java.util.concurrent.atomic包下,一系列以Atomic開頭的包裝類。例如:AtomicBoolean,AtomicInteger,AtomicLong。它們分別用于Boolean,Integer,Long類型的原子性操作。
為了方面對這些類逐級掌握,我將這些原子類型分為以下幾類:
- 普通原子類型:提供對boolean、int、long和對象的原子性操作。
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
- 原子類型數(shù)組:提供對數(shù)組元素的原子性操作。
- AtomicLongArray
- AtomicIntegerArray
- AtomicReferenceArray
- 原子類型字段更新器:提供對指定對象的指定字段進(jìn)行原子性操作。
- AtomicLongFieldUpdater
- AtomicIntegerFieldUpdater
- AtomicReferenceFieldUpdater
- 帶版本號的原子引用類型:以版本戳的方式解決原子類型的ABA問題。
- AtomicStampedReference
- AtomicMarkableReference
- 原子累加器(JDK1.8):AtomicLong和AtomicDouble的升級類型,專門用于數(shù)據(jù)統(tǒng)計,性能更高。
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
原子類型累加器是JDK1.8引進(jìn)的并發(fā)新技術(shù),它可以看做AtomicLong和AtomicDouble的部分加強(qiáng)類型。低并發(fā)、一般的業(yè)務(wù)場景下AtomicLong是足夠了。如果并發(fā)量很多,存在大量寫多讀少的情況,那LongAdder可能更合適,代價是消耗更多的內(nèi)存空間。
AtomicLong中有個內(nèi)部變量value保存著實(shí)際的long值,所有的操作都是針對該變量進(jìn)行。也就是說,高并發(fā)環(huán)境下,value變量其實(shí)是一個熱點(diǎn),也就是N個線程競爭一個熱點(diǎn)。在并發(fā)量較低的環(huán)境下,線程沖突的概率比較小,自旋的次數(shù)不會很多。但是,高并發(fā)環(huán)境下,N個線程同時進(jìn)行自旋操作,會出現(xiàn)大量失敗并不斷自旋的情況,此時AtomicLong的自旋會成為瓶頸。
這就是LongAdder引入的初衷——解決高并發(fā)環(huán)境下AtomicLong的自旋瓶頸問題。
LongAdder的基本思路就是分散熱點(diǎn),將value值分散到一個數(shù)組中,不同線程會命中到數(shù)組的不同槽中,各個線程只對自己槽中的那個值進(jìn)行CAS操作,這樣熱點(diǎn)就被分散了,沖突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。這種做法有沒有似曾相識的感覺?沒錯,
ConcurrentHashMap中的“分段鎖”其實(shí)就是類似的思路。
參考鏈接:
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
Springboot項(xiàng)目中如何讓非Spring管理的類獲得一個注入的Bean
這篇文章主要介紹了Springboot項(xiàng)目中如何讓非Spring管理的類獲得一個注入的Bean問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12Java SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證
這篇文章主要介紹了Java SpringSecurity+JWT實(shí)現(xiàn)登錄認(rèn)證,首先通過給需要登錄認(rèn)證的模塊添加mall-security依賴展開介紹,感興趣的朋友可以參考一下2022-06-06SpringBoot使用過濾器、攔截器和監(jiān)聽器的案例代碼(Springboot搭建java項(xiàng)目)
這篇文章主要介紹了SpringBoot使用過濾器、攔截器和監(jiān)聽器(Springboot搭建java項(xiàng)目),本文是基于Springboot搭建java項(xiàng)目,結(jié)合案例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-02-02Spring請求路徑帶參數(shù)URL使用注解的寫法說明
這篇文章主要介紹了Spring請求路徑帶參數(shù)URL使用注解的寫法說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08Mybatis插件+注解實(shí)現(xiàn)數(shù)據(jù)脫敏方式
這篇文章主要介紹了Mybatis插件+注解實(shí)現(xiàn)數(shù)據(jù)脫敏方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09詳解Spring Boot 自定義PropertySourceLoader
這篇文章主要介紹了詳解Spring Boot 自定義PropertySourceLoader,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05