告訴你一個(gè) AtomicInteger 的驚天大秘密!
i++ 不是線程安全的操作,因?yàn)樗皇且粋€(gè)原子性操作。
那么,如果我想要達(dá)到類似 i++ 的這種效果,我應(yīng)該使用哪些集合或者說工具類呢?
在 JDK1.5 之前,為了確保在多線程下對某基本數(shù)據(jù)類型或者引用數(shù)據(jù)類型運(yùn)算的原子性,必須依賴于外部關(guān)鍵字 synchronized,但是這種情況在 JDK1.5 之后發(fā)生了改觀,當(dāng)然你依然可以使用 synchronized 來保證原子性,我們這里所說的一種線程安全的方式是原子性的工具類,比如 「AtomicInteger、AtomicBoolean」 等。這些原子類都是線程安全的工具類,他們同時(shí)也是 Lock-Free 的。下面我們就來一起認(rèn)識一下這些工具類以及 Lock - Free 是個(gè)什么概念。
了解 AtomicInteger
AtomicInteger 是 JDK1.5 新添加的工具類,我們首先來看一下它的繼承關(guān)系
與 int 的包裝類 Integer 一樣,都是繼承于 Number 類的。
這個(gè) Number 類是基本數(shù)據(jù)類型的包裝類,一般和數(shù)據(jù)類型有關(guān)的對象都會(huì)繼承于 Number 類。
它的繼承體系很簡單,下面我們來看一下它的基本屬性和方法
AtomicInteger 的基本屬性
AtomicInteger 的基本屬性有三個(gè)
Unsafe 是 sun.misc 包下面的類,AtomicInteger 主要是依賴于 sun.misc.Unsafe 提供的一些 native 方法保證操作的原子性。
Unsafe 的 objectFieldOffset 方法可以獲取成員屬性在內(nèi)存中的地址相對于對象內(nèi)存地址的偏移量。說得簡單點(diǎn)就是找到這個(gè)變量在內(nèi)存中的地址,便于后續(xù)通過內(nèi)存地址直接進(jìn)行操作,這個(gè)值就是 value這個(gè)我們后面會(huì)再細(xì)說
value 就是 AtomicIneger 的值。
AtomicInteger 的構(gòu)造方法
繼續(xù)往下看,AtomicInteger 的構(gòu)造方法只有兩個(gè),一個(gè)是無參數(shù)的構(gòu)造方法,無參數(shù)的構(gòu)造方法默認(rèn)的 value 初始值是 0 ,帶參數(shù)的構(gòu)造方法可以指定初始值。
AtomicInteger 中的方法
下面我們就來聊一下 AtomicInteger 中的方法。
Get 和 Set
我們首先來看一下最簡單的 get 、set 方法:
get() : 獲取當(dāng)前 AtomicInteger 的值
set() : 設(shè)置當(dāng)前 AtomicInteger 的值
get() 可以原子性的讀取 AtomicInteger 中的數(shù)據(jù),set() 可以原子性的設(shè)置當(dāng)前的值,因?yàn)?get() 和 set() 最終都是作用于 value 變量,而 value 是由 volatile 修飾的,所以 get 、set 相當(dāng)于都是對內(nèi)存進(jìn)行讀取和設(shè)置。如下圖所示
我們上面提到了 i++ 和 i++ 的非原子性操作,我們說可以使用 AtomicInteger 中的方法進(jìn)行替換。
Incremental 操作
AtomicInteger 中的 Incremental 相關(guān)方法可以滿足我們的需求
- getAndIncrement() : 原子性的增加當(dāng)前的值,并把結(jié)果返回。相當(dāng)于 i++的操作。
為了驗(yàn)證是不是線程安全的,我們用下面的例子進(jìn)行測試
- public class TAtomicTest implements Runnable{
- AtomicInteger atomicInteger = new AtomicInteger();
- @Override
- public void run() {
- for(int i = 0;i < 10000;i++){
- System.out.println(atomicInteger.getAndIncrement());
- }
- }
- public static void main(String[] args) {
- TAtomicTest tAtomicTest = new TAtomicTest();
- Thread t1 = new Thread(tAtomicTest);
- Thread t2 = new Thread(tAtomicTest);
- t1.start();
- t2.start();
- }
- }
通過輸出結(jié)果你會(huì)發(fā)現(xiàn)它是一個(gè)線程安全的操作,你可以修改 i 的值,但是最后的結(jié)果仍然是 i - 1,因?yàn)橄热≈?,然后?+ 1,它的示意圖如下。
- incrementAndGet 與此相反,首先執(zhí)行 + 1 操作,然后返回自增后的結(jié)果,該操作方法能夠確保對 value 的原子性操作。如下圖所示
Decremental 操作
與此相對,x-- 或者 x = x - 1 這樣的自減操作也是原子性的。我們?nèi)匀豢梢允褂?AtomicInteger 中的方法來替換
- getAndDecrement : 返回當(dāng)前類型的 int 值,然后對 value 的值進(jìn)行自減運(yùn)算。下面是測試代碼
- class TAtomicTestDecrement implements Runnable{
- AtomicInteger atomicInteger = new AtomicInteger(20000);
- @Override
- public void run() {
- for(int i = 0;i < 10000 ;i++){
- System.out.println(atomicInteger.getAndDecrement());
- }
- }
- public static void main(String[] args) {
- TAtomicTestDecrement tAtomicTest = new TAtomicTestDecrement();
- Thread t1 = new Thread(tAtomicTest);
- Thread t2 = new Thread(tAtomicTest);
- t1.start();
- t2.start();
- }
- }
下面是 getAndDecrement 的示意圖
- decrementAndGet:同樣的,decrementAndGet 方法就是先執(zhí)行遞減操作,然后再獲取 value 的值,示意圖如下
LazySet方法
volatile 有內(nèi)存屏障你知道嗎?
內(nèi)存屏障是啥啊?
內(nèi)存屏障,也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等, 是一類同步屏障指令,是 CPU 或編譯器在對內(nèi)存隨機(jī)訪問的操作中的一個(gè)同步點(diǎn),使得此點(diǎn)之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點(diǎn)之后的操作。也是一個(gè)讓CPU 處理單元中的內(nèi)存狀態(tài)對其它處理單元可見的一項(xiàng)技術(shù)。
CPU 使用了很多優(yōu)化,使用緩存、指令重排等,其最終的目的都是為了性能,也就是說,當(dāng)一個(gè)程序執(zhí)行時(shí),只要最終的結(jié)果是一樣的,指令是否被重排并不重要。所以指令的執(zhí)行時(shí)序并不是順序執(zhí)行的,而是亂序執(zhí)行的,這就會(huì)帶來很多問題,這也促使著內(nèi)存屏障的出現(xiàn)。
語義上,內(nèi)存屏障之前的所有寫操作都要寫入內(nèi)存;內(nèi)存屏障之后的讀操作都可以獲得同步屏障之前的寫操作的結(jié)果。因此,對于敏感的程序塊,寫操作之后、讀操作之前可以插入內(nèi)存屏障。
內(nèi)存屏障的開銷非常輕量級,但是再小也是有開銷的,LazySet 的作用正是如此,它會(huì)以普通變量的形式來讀寫變量。
也可以說是:「懶得設(shè)置屏障了」
GetAndSet 方法
以原子方式設(shè)置為給定值并返回舊值。
它的源碼就是調(diào)用了一下 unsafe 中的 getAndSetInt 方法,如下所示
就是先進(jìn)行循環(huán),然后調(diào)用 getIntVolatile 方法,這個(gè)方法我在 cpp 中沒有找到,找到的小伙伴們記得及時(shí)告訴讓我學(xué)習(xí)一下。
循環(huán)直到 compareAndSwapInt 返回 false,這就說明使用 CAS 并沒有更新為新的值,所以 var5 返回的就是最新的內(nèi)存值。
CAS 方法
我們一直常說的 CAS 其實(shí)就是 CompareAndSet 方法,這個(gè)方法顧名思義,就是 「比較并更新」 的意思,當(dāng)然這是字面理解,字面理解有點(diǎn)偏差,其實(shí)人家的意思是先比較,如果滿足那么再進(jìn)行更新。
上面給出了 CAS Java 層面的源碼,JDK 官方給它的解釋就是 「如果當(dāng)前值等于 expect 的值,那么就以原子性的方式將當(dāng)前值設(shè)置為 update 給定值」,這個(gè)方法會(huì)返回一個(gè) boolean 類型,如果是 true 就表示比較并更新成功,否則表示失敗。
CAS 同時(shí)也是一種無鎖并發(fā)機(jī)制,也稱為 Lock Free,所以你覺得 Lock Free 很高大上嗎?并沒有。
下面我們構(gòu)建一個(gè)加鎖解鎖的 CASLock
- class CASLock {
- AtomicInteger atomicInteger = new AtomicInteger();
- Thread currentThread = null;
- public void tryLock() throws Exception{
- boolean isLock = atomicInteger.compareAndSet(0, 1);
- if(!isLock){
- throw new Exception("加鎖失敗");
- }
- currentThread = Thread.currentThread();
- System.out.println(currentThread + " tryLock");
- }
- public void unlock() {
- int lockValue = atomicInteger.get();
- if(lockValue == 0){
- return;
- }
- if(currentThread == Thread.currentThread()){
- atomicInteger.compareAndSet(1,0);
- System.out.println(currentThread + " unlock");
- }
- }
- public static void main(String[] args) {
- CASLock casLock = new CASLock();
- for(int i = 0;i < 5;i++){
- new Thread(() -> {
- try {
- casLock.tryLock();
- Thread.sleep(10000);
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- casLock.unlock();
- }
- }).start();
- }
- }
- }
在上面的代碼中,我們構(gòu)建了一個(gè) CASLock,在 tryLock 方法中,我們先使用 CAS 方法進(jìn)行更新,如果更新不成功則拋出異常,并把當(dāng)前線程設(shè)置為加鎖線程。在 unLock 方法中,我們先判斷當(dāng)前值是否為 0 ,如果是 0 就是我們愿意看到的結(jié)果,直接返回。否則是 1,則表示當(dāng)前線程還在加鎖,我們再來判斷一下當(dāng)前線程是否是加鎖線程,如果是則執(zhí)行解鎖操作。
那么我們上面提到的 compareAndSet,它其實(shí)可以解析為如下操作
- // 偽代碼
- // 當(dāng)前值
- int v = 0;
- int a = 0;
- int b = 1;
- if(compare(0,0) == true){
- set(0,1);
- }
- else{
- // 繼續(xù)向下執(zhí)行
- }
也可以拿生活場景中的買票舉例子,你去景區(qū)旅游肯定要持票才能進(jìn),如果你拿著是假票或者不符合景區(qū)的票肯定是能夠被識別出來的,如果你沒有拿票拿你也肯定進(jìn)不去景區(qū)。
廢話少說,這就祭出來 compareAndSet 的示意圖
weakCompareAndSet: 媽的非常認(rèn)真看了好幾遍,發(fā)現(xiàn) JDK1.8 的這個(gè)方法和 compareAndSet 方法完全一摸一樣啊,坑我。。。
但是真的是這樣么?并不是,JDK 源碼很博大精深,才不會(huì)設(shè)計(jì)一個(gè)重復(fù)的方法,你想想 JDK 團(tuán)隊(duì)也不是會(huì)犯這種低級團(tuán)隊(duì),但是原因是什么呢?
《Java 高并發(fā)詳解》這本書給出了我們一個(gè)答案
AddAndGet
AddAndGet 和 getAndIncrement、getAndAdd、incrementAndGet 等等方法都是使用了 do ... while + CAS 操作,其實(shí)也就相當(dāng)于是一個(gè)自旋鎖,如果 CAS 修改成功就會(huì)一直循環(huán),修改失敗才會(huì)返回。示意圖如下
深入 AtomicInteger
我們上面探討了 AtomicInteger 的具體使用,同時(shí)我們知道 AtomicInteger 是依靠 volatile 和 CAS 來保證原子性的,那么我們下面就來分析一下為什么 CAS 能夠保證原子性,它的底層是什么?AtomicInteger 與樂觀鎖又有什么關(guān)系呢?
AtomicInteger 的底層實(shí)現(xiàn)原理我們再來瞧瞧這個(gè)可愛的 compareAndSetL(CAS) 方法,為什么就這兩行代碼就保證原子性了?
我們可以看到,這個(gè) CAS 方法相當(dāng)于是調(diào)用了 unsafe 中的 compareAndSwapInt 方法,我們進(jìn)到 unsafe 方能發(fā)中看一下具體實(shí)現(xiàn)。
compareAndSwapInt 是 sun.misc 中的方法,這個(gè)方法是一個(gè) native 方法,它的底層是 C/C++ 實(shí)現(xiàn)的,所以我們需要看 C/C++ 的源碼。
知道 C/C++ 的牛逼之處了么。使用 Java 就是玩應(yīng)用和架構(gòu)的,C/C++ 是玩服務(wù)器、底層的。
compareAndSwapInt 的源碼在 jdk8u-dev/hotspot/src/share/vm/prims/unsafe.app 路徑下,它的源碼實(shí)現(xiàn)是
也就是 Unsafe_CompareAndSwapInt 方法,我們找到這個(gè)方法
C/C++ 源碼我也看不懂,但是這不妨礙我們找到關(guān)鍵代碼 Atomic::cmpxchg ,cmpxchg 是 x86 CPU 架構(gòu)的匯編指令,它的主要作用就是比較并交換操作數(shù)。我們繼續(xù)往下跟找一下這個(gè)指令的定義。
我們會(huì)發(fā)現(xiàn)對應(yīng)不同的 os,其底層實(shí)現(xiàn)方式不一樣
我們找到 Windows 的實(shí)現(xiàn)方式如下
我們繼續(xù)向下找,它其實(shí)定義的是第 216 行的代碼,我們找進(jìn)去
此時(shí)就需要匯編指令和寄存器相關(guān)的知識了。
上面的 os::is-MP() 是多處理操作系統(tǒng)的接口,下面是 __asm ,它是 C/C++ 的關(guān)鍵字,用于調(diào)用內(nèi)聯(lián)匯編程序。
__asm 中的代碼是匯編程序,大致來說就是把 dest、exchange_value 、compare_value 的值都放在寄存器中,下面的 LOCK_IF_MP 中代碼的大致意思就是
如果是多處理器的話就會(huì)執(zhí)行 lock,然后進(jìn)行比較操作。其中的 cmp 表示比較,mp 表示的就是 MultiProcess,je 表示相等跳轉(zhuǎn),L0 表示的是標(biāo)識位。
我們回到上面的匯編指令,我們可以看到,CAS 的底層就是 cmpxchg 指令。
樂觀鎖
你有沒有這個(gè)疑問,為什么 AtomicInteger 可以獲取當(dāng)前值,那為什么還會(huì)出現(xiàn) expectValue 和 value 不一致的情況呢?
因?yàn)?AtomicInteger 只是一個(gè)原子性的工具類,它不具有排他性,它不像是 synchronized 或者是 lock 一樣具有互斥和排他性,還記得 AtomicInteger 中有兩個(gè)方法 get 和 set 嗎?它們只是用 volatile 修飾了一下,而 volatile 不具有原子性,所以可能會(huì)存在 expectValue 和 value 的當(dāng)前值不一致的情況,因此可能會(huì)出現(xiàn)重復(fù)修改。
針對上面這種情況的解決辦法有兩種,一種是使用 synchronized 和 lock 等類似的加鎖機(jī)制,這種鎖具有獨(dú)占性,也就是說同一時(shí)刻只能有一個(gè)線程來進(jìn)行修改,這種方式能夠保證原子性,但是相對開銷比較大,這種鎖也叫做悲觀鎖。另外一種解決辦法是使用版本號或者是 CAS 方法。
「版本號」
版本號機(jī)制是在數(shù)據(jù)表中加上一個(gè) version 字段來實(shí)現(xiàn)的,表示數(shù)據(jù)被修改的次數(shù),當(dāng)執(zhí)行寫操作并且寫入成功后,version = version + 1,當(dāng)線程 A 要更新數(shù)據(jù)時(shí),在讀取數(shù)據(jù)的同時(shí)也會(huì)讀取 version 值,在提交更新時(shí),若剛才讀取到的 version 值為當(dāng)前數(shù)據(jù)庫中的 version 值相等時(shí)才更新,否則重試更新操作,直到更新成功。
「CAS 方法」
還有一種方式就是 CAS 了,我們上面用了大量的篇幅來介紹 CAS 方法,那么我們認(rèn)為你現(xiàn)在已經(jīng)對其運(yùn)行機(jī)制有一定的了解了,我們就不再闡述它的運(yùn)行機(jī)制了。
任何事情都是有利也有弊,軟件行業(yè)沒有完美的解決方案只有最優(yōu)的解決方案,所以樂觀鎖也有它的弱點(diǎn)和缺陷,那就是 ABA 問題。
ABA 問題
ABA 問題說的是,如果一個(gè)變量第一次讀取的值是 A,準(zhǔn)備好需要對 A 進(jìn)行寫操作的時(shí)候,發(fā)現(xiàn)值還是 A,那么這種情況下,能認(rèn)為 A 的值沒有被改變過嗎?可以是由 A -> B -> A 的這種情況,但是 AtomicInteger 卻不會(huì)這么認(rèn)為,它只相信它看到的,它看到的是什么就是什么。舉個(gè)例子來說
假如現(xiàn)在有一個(gè)單鏈表,如下圖所示
A.next = B ,B.next = null,此時(shí)有兩個(gè)線程 T1 和 T2 分別從單鏈表中取出 A ,由于一些特殊原因,T2 把 A 改為 B ,然后又改為 A ,此時(shí) T1 執(zhí)行 CAS 方法,發(fā)現(xiàn)單鏈表仍然是 A ,就會(huì)執(zhí)行 CAS 方法,雖然結(jié)果沒錯(cuò),但是這種操作會(huì)造成一些潛在的問題。
此時(shí)還是一個(gè)單鏈表,兩個(gè)線程 T1 和 T2 分別從單鏈表中取出 A ,然后 T1 把鏈表改為 ACD 如下圖所示
此時(shí) T2,發(fā)現(xiàn)內(nèi)存值還是 A ,就會(huì)把 A 的值嘗試替換為 B ,因?yàn)?B 的引用是 null,此時(shí)就會(huì)造成 C、D 處于游離態(tài)
JDK 1.5 以后的 AtomicStampedReference類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當(dāng)前值是否等于預(yù)期值,判斷的標(biāo)準(zhǔn)就是當(dāng)前引用和郵戳分別和預(yù)期引用和郵戳相等,如果全部相等,則以原子方式設(shè)置為給定的更新值。
好了,上面就是 Java 代碼流程了,看到 native 我們知道又要擼 cpp 了。開擼
簡單解釋一下就是 UnsafeWrapper 就是包裝器,換個(gè)名字而已。然后經(jīng)過一些 JNI 的處理,因?yàn)?compareAndSwapOject 比較的是引用,所以需要經(jīng)過 C++ 面向?qū)ο蟮霓D(zhuǎn)換。最主要的方法是 atomic_compare_exchange_oop
可以看到,又出現(xiàn)了熟悉的詞匯 cmpxchg ,也就是說 compareAndSwapOject 使用的還是 cmpxchg 原子性指令,只是它經(jīng)過了一系列轉(zhuǎn)換。
后記
拋出來一個(gè)問題,CAS 能保證變量之間的可見性么?為什么?
還有一個(gè)問題,getIntVolatile 方法的 cpp 源碼在哪里?怎么找?
本文轉(zhuǎn)載自微信公眾號「Java建設(shè)者」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系Java建設(shè)者公眾號。