前言: synchronized保證了線程安全,但是在某些情況下,卻不是一個最優選擇,關鍵在于性能問題。Java中提供了很多原子操作類來保證共享變量操作的原子性。這些原子操作的底層原理都是使用了CAS機制。既然用鎖或 synchronized 關鍵字可以實現原子操作,那么為什么還要用 CAS 呢,因為加鎖或使用 synchronized 關鍵字帶來的性能損耗較大,而用 CAS 可以實現樂觀鎖,它實際上是直接利用了 CPU 層面的指令,沒有加鎖和線程上下文切換的開銷,所以性能很高。
一、CAS機制簡介
1.1、悲觀鎖和樂觀鎖更新數據方式
CAS機制是一種數據更新的方式。在具體講什么是CAS機制之前,我們先來聊下在多線程環境下,對共享變量進行數據更新的兩種模式:悲觀鎖模式和樂觀鎖模式。
悲觀鎖更新的方式認為:在更新數據的時候大概率會有其他線程去爭奪共享資源,所以悲觀鎖的做法是:第一個獲取資源的線程會將資源鎖定起來,其他沒爭奪到資源的線程只能進入阻塞隊列,等第一個獲取資源的線程釋放鎖之后,這些線程才能有機會重新爭奪資源。synchronized就是Java中悲觀鎖的典型實現,synchronized使用起來非常簡單方便,但是會使沒爭搶到資源的線程進入阻塞狀態,線程在阻塞狀態和Runnable狀態之間切換效率較低(比較慢)。比如你的更新操作其實是非常快的,這種情況下你還用synchronized將其他線程都鎖住了,線程從Blocked狀態切換回Runnable華的時間可能比你的更新操作的時間還要長。
樂觀鎖更新方式認為:在更新數據的時候其他線程爭搶這個共享變量的概率非常小,所以更新數據的時候不會對共享數據加鎖。但是在正式更新數據之前會檢查數據是否被其他線程改變過,如果未被其他線程改變過就將共享變量更新成最新值,如果發現共享變量已經被其他線程更新過了,就重試,直到成功為止。CAS機制就是樂觀鎖的典型實現。
1.2、什么是CAS機制
CAS,是Compare and Swap的簡稱,是一種用于在多線程環境下實現同步功能的機制。CAS 操作包含三個操作數 -- 內存位置、預期數值和新值。CAS 的實現邏輯是將內存位置處的數值與預期數值相比較,若相等,則將內存位置處的值替換為新值。若不相等,則不做任何操作。
在 Java 中,Java 并沒有直接實現 CAS,CAS 相關的實現是通過 C++ 內聯匯編的形式實現的。Java 代碼需通過 JNI 才能調用。
CAS這個機制中有三個核心的參數:
主內存中存放的共享變量的值:V(一般情況下這個V是內存的地址值,通過這個地址可以獲得內存中的值)
工作內存中共享變量的副本值,也叫預期值:A
需要將共享變量更新到的最新值:B
如上圖中,主存中保存V值,線程中要使用V值要先從主存中讀取V值到線程的工作內存A中,然后計算后變成B值,最后再把B值寫回到內存V值中。多個線程共用V值都是如此操作。CAS的核心是在將B值寫入到V之前要比較A值和V值是否相同,如果不相同證明此時V值已經被其他線程改變,重新將V值賦給A,并重新計算得到B,如果相同,則將B值賦給V。
值得注意的是CAS機制中的這步步驟是原子性的(從指令層面提供的原子操作),所以CAS機制可以解決多線程并發編程對共享變量讀寫的原子性問題。
1.3、CAS與sychronized比較
從思想上來說:
①. synchronized屬于【悲觀鎖】
悲觀鎖認為:程序中的【并發】情況嚴重,所以【嚴防死守】
②. CAS屬于【樂觀鎖】
樂觀鎖認為:程序中的【并發】情況不那么嚴重,所以讓【線程不斷去嘗試更新】
這2種機制沒有絕對的好與壞,關鍵看使用場景。在并發量非常高的情況下,反而用同步鎖更合適一些。
1.4、Java中都有哪些地方應用到了CAS機制呢?
a、Atomic系列類
b、Lock系列類底層實現
c、Java1.6以上版本,synchronized轉變為重量級鎖之前,也會采用CAS機制
1.5、CAS 實現自旋鎖
既然用鎖或 synchronized 關鍵字可以實現原子操作,那么為什么還要用 CAS 呢,因為加鎖或使用 synchronized 關鍵字帶來的性能損耗較大,而用 CAS 可以實現樂觀鎖,它實際上是直接利用了 CPU 層面的指令,沒有加鎖和線程上下文切換的開銷,所以性能很高。
上面也說了,CAS 是實現自旋鎖的基礎,CAS 利用 CPU 指令保證了操作的原子性,以達到鎖的效果,至于自旋呢,看字面意思也很明白,自己旋轉,翻譯成人話就是循環,一般是用一個無限循環實現。這樣一來,一個無限循環中,執行一個 CAS 操作,當操作成功,返回 true 時,循環結束;當返回 false 時,接著執行循環,繼續嘗試 CAS 操作,直到返回 true。
其實 JDK 中有好多地方用到了 CAS ,尤其是 java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic 包下,相信大家都用到過 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。
1.6、CAS機制優缺點
CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操作
CAS機制CAS機制缺點
1>ABA問題
ABA問題:CAS在操作的時候會檢查變量的值是否被更改過,如果沒有則更新值,但是帶來一個問題,最開始的值是A,接著變成B,最后又變成了A。經過檢查這個值確實沒有修改過,因為最后的值還是A,但是實際上這個值確實已經被修改過了。為了解決這個問題,在每次進行操作的時候加上一個版本號,每次操作的就是兩個值,一個版本號和某個值,A——>B——>A問題就變成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference類解決ABA問題,用Pair這個內部類實現,包含兩個屬性,分別代表版本號和引用,在compareAndSet中先對當前引用進行檢查,再對版本號標志進行檢查,只有全部相等才更新值。
AtomicStampedReference
和AtomicMarkableReference
就是用來解決CAS中的ABA問題的。他們解決ABA問題的原理類似,都是通過一個版本號來區分有沒被更新過。
AtomicStampedReference
:帶版本戳的原子引用類型,版本戳為int類型。
AtomicMarkableReference
:帶版本戳的原子引用類型,版本戳為boolean類型。
2>可能會消耗較高的CPU
看起來CAS比鎖的效率高,從阻塞機制變成了非阻塞機制,減少了線程之間等待的時間。每個方法不能絕對的比另一個好,在線程之間競爭程度大的時候,如果使用CAS,每次都有很多的線程在競爭,也就是說CAS機制不能更新成功。這種情況下CAS機制會一直重試,這樣就會比較耗費CPU。因此可以看出,如果線程之間競爭程度小,使用CAS是一個很好的選擇;但是如果競爭很大,使用鎖可能是個更好的選擇。在并發量非常高的環境中,如果仍然想通過原子類來更新的話,可以使用AtomicLong的替代類:LongAdder。
3>不能保證代碼塊的原子性
Java中的CAS機制只能保證一個共享變量的原子操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。
CAS機制優點
可以保證變量操作的原子性;
并發量不是很高的情況下,使用CAS機制比使用鎖機制效率更高;
在線程對共享資源占用時間較短的情況下,使用CAS機制效率也會較高。
二、Java提供的CAS操作類--Unsafe類
2.1、Unsafe類簡介
在研究JDK中AQS時,會發現這個類很多地方都使用了CAS操作,在并發實現中CAS操作必須具備原子性,而且是硬件級別的原子性,Java被隔離在硬件之上,明顯力不從心,這時為了能直接操作操作系統層面,肯定要通過用C++編寫的native本地方法來擴展實現。JDK提供了一個類來滿足CAS的要求,sun.misc.Unsafe,從名字上可以大概知道它用于執行低級別、不安全的操作,AQS就是使用此類完成硬件級別的原子操作。UnSafe通過JNI調用本地C++代碼,C++代碼調用CPU硬件指令集。
Unsafe是一個很強大的類,它可以分配內存、釋放內存、可以定位對象某字段的位置、可以修改對象的字段值、可以使線程掛起、使線程恢復、可進行硬件級別原子的CAS操作等等。
從Java5開始引入了對CAS機制的底層的支持,在這之前需要開發人員編寫相關的代碼才可以實現CAS。在原子變量類Atomic中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代碼,在這里的代碼都是調用了底層(核心代碼調用native修飾的方法)的實現方法。
在AtomicInteger源碼中可以看getAndSet方法和compareAndSet方法之間的關系,compareAndSet方法調用了底層的實現,該方法可以實現與一個volatile變量的讀取和寫入相同的效果。在前面說到了volatile不支持例如i++這樣的復合操作,在Atomic中提供了實現該操作的方法。JVM對CAS的支持通過這些原子類(Atomic***)暴露出來,供我們使用。
而Atomic系類的類底層調用的是Unsafe類的API,Unsafe類提供了一系列的compareAndSwap*方法,下面就簡單介紹下Unsafe類的API:
long objectFieldOffset(Field field)方法:返回指定的變量在所屬類中的內存偏移地址,該偏移地址僅僅在該Unsafe函數中訪問指定字段時使用。如下代碼使用Unsafe類獲取變量value在AtomicLong對象中的內存偏移。
static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
-
int arrayBaseOffset(Class arrayClass)
方法:獲取數組中第一個元素的地址。 -
int arrayIndexScale(Class arrayClass)
方法:獲取數組中一個元素占用的字節。 -
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的變量對應volatile語義的值。 -
void putLongvolatile(Object obj, long offset, long value)
方法:設置obj對象中offset偏移的類型為long的field的值為value,支持volatile語義。 -
void putOrderedLong(Object obj, long offset, long value)
方法:設置obj對象中offset偏移地址對應的long型field的值為value。這是一個有延遲的putLongvolatile方法,并且不保證值修改對其他線程立刻可見。只有在變量使用volatile修飾并且預計會被意外修改時才使用該方法。 -
void park(boolean isAbsolute, long time)
方法:阻塞當前線程,其中參數isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞線程會被喚醒,這個time是個相對值,是個增量值,也就是相對當前時間累加time后當前線程就會被喚醒。如果isAbsolute等于true,并且time大于0,則表示阻塞的線程到指定的時間點后會被喚醒,這里time是個絕對時間,是將某個時間點換算為ms后的值。另外,當其他線程調用了當前阻塞線程的interrupt方法而中斷了當前線程時,當前線程也會返回,而當其他線程調用了unPark方法并且把當前線程作為參數時當前線程也會返回。 -
void unpark(Object thread)
方法:喚醒調用park后阻塞的線程。
下面是JDK8新增的函數,這里只列出Long類型操作。
long getAndSetLong(Object obj, long offset, long update)
方法:獲取對象obj中偏移量為offset的變量volatile語義的當前值,并設置變量volatile語義的值為update。
//這個方法只是封裝了compareAndSwapLong的使用,不需要自己寫重試機制 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語義的當前值,并設置變量值為原始值+addValue,原理和上面的方法類似。
2.2、Unsafe類的使用
三、CAS使用場景
- 使用一個變量統計網站的訪問量;
- Atomic類操作;
- 數據庫樂觀鎖更新。
3.1、使用一個變量統計網站的訪問量
要實現一個網站訪問量的計數器,可以通過一個Long類型的對象,并加上synchronized內置鎖的方式。但是這種方式使得多線程的訪問變成了串行的,同一時刻只能有一個線程可以更改long的值,那么為了能夠使多線程并發的更新long的值,我們可以使用J.U.C包中的Atomic原子類。這些類的更新是原子的,不需要加鎖即可實現并發的更新,并且是線程安全的。
可是Atomic原子類是怎么保證并發更新的線程安全的呢?讓我們看一下AtomicLong的自增方法incrementAndGet():
public final long incrementAndGet() { // 無限循環,即自旋 for (;;) { // 獲取主內存中的最新值 long current = get(); long next = current + 1; // 通過CAS原子更新,若能成功則返回,否則繼續自旋 if (compareAndSet(current, next)) return next; } } private volatile long value; public final long get() { return value; }
可以發現其內部保持著一個volatile修飾的long變量,volatile保證了long的值更新后,其他線程能立即獲得最新的值。
在incrementAndGet中首先是一個無限循環(自旋),然后獲取long的最新值,將long加1,然后通過compareAndSet()方法嘗試將long的值有current更新為next。如果能更新成功,則說明當前還沒有其他線程更新該值,則返回next,如果更新失敗,則說明有其他線程提前更新了該值,則當前線程繼續自旋嘗試更新。
簡單總結
總體來說,AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference原理比較簡單:使用CAS保證原子性,使用volatile保證可見性,最終能保證共享變量操作的線程安全。
AtomicLongArray、AtomicIntArray和AtomicReferenceArray的實現原理略有不同,是用CAS機制配合final機制來實現共享變量操作的線程安全的。感興趣的同學可以自己分析下,也是比較簡單的。
CAS的操作其底層是通過調用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的,需要調用JNI接口,也即通過操作系統來保證這些方法的執行。
3.2、現在我們嘗試在代碼中引入AtomicInteger類
在使用Integer的時候,必須加上synchronized保證不會出現并發線程同時訪問的情況
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() { //每個線程當中讓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保證不會出現并發線程同時訪問的情況,否則結果可能只有1千多 public synchronized static void increment() { count++; } }
而在AtomicInteger中卻不用加上synchronized,在這里AtomicInteger是提供原子操作的
在某些情況下,原子類代碼的性能會比Synchronized更好,因為沒有加鎖的線程同步上下文切換開銷,底層采用了CAS機制保證共享變量原子性,還配合volatile保證內存可見性,最終能保證共享變量操作的線程安全。
四、Java中的原子操作類
在JDK1.5版本之前,多行代碼的原子性主要通過synchronized關鍵字進行保證。在JDK1.5版本,Java提供了原子類型專門確保變量操作的原子性。所謂的原子操作類,指的是java.util.concurrent.atomic包下,一系列以Atomic開頭的包裝類。例如:AtomicBoolean,AtomicInteger,AtomicLong。它們分別用于Boolean,Integer,Long類型的原子性操作。
為了方面對這些類逐級掌握,我將這些原子類型分為以下幾類:
-
普通原子類型:提供對boolean、int、long和對象的原子性操作。
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
-
原子類型數組:提供對數組元素的原子性操作。
- AtomicLongArray
- AtomicIntegerArray
- AtomicReferenceArray
-
原子類型字段更新器:提供對指定對象的指定字段進行原子性操作。
- AtomicLongFieldUpdater
- AtomicIntegerFieldUpdater
- AtomicReferenceFieldUpdater
-
帶版本號的原子引用類型:以版本戳的方式解決原子類型的ABA問題。
- AtomicStampedReference
- AtomicMarkableReference
-
原子累加器(JDK1.8):AtomicLong和AtomicDouble的升級類型,專門用于數據統計,性能更高。
- DoubleAccumulator
- DoubleAdder
- LongAccumulator
- LongAdder
原子類型累加器是JDK1.8引進的并發新技術,它可以看做AtomicLong和AtomicDouble的部分加強類型。低并發、一般的業務場景下AtomicLong是足夠了。如果并發量很多,存在大量寫多讀少的情況,那LongAdder可能更合適,代價是消耗更多的內存空間。
AtomicLong中有個內部變量value保存著實際的long值,所有的操作都是針對該變量進行。也就是說,高并發環境下,value變量其實是一個熱點,也就是N個線程競爭一個熱點。在并發量較低的環境下,線程沖突的概率比較小,自旋的次數不會很多。但是,高并發環境下,N個線程同時進行自旋操作,會出現大量失敗并不斷自旋的情況,此時AtomicLong的自旋會成為瓶頸。
這就是LongAdder引入的初衷——解決高并發環境下AtomicLong的自旋瓶頸問題。
LongAdder的基本思路就是分散熱點,將value值分散到一個數組中,不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,沖突的概率就小很多。如果要獲取真正的long值,只要將各個槽中的變量值累加返回。這種做法有沒有似曾相識的感覺?沒錯,
ConcurrentHashMap中的“分段鎖”其實就是類似的思路。
參考鏈接:
原子類型累加載器
總結
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注服務器之家的更多內容!
原文鏈接:https://blog.csdn.net/CSDN2497242041/article/details/120213073