日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
告訴你一個 AtomicInteger 的驚天大秘密!

 i++ 不是線程安全的操作,因?yàn)樗皇且粋€原子性操作。

創(chuàng)新互聯(lián)公司是專業(yè)的利通網(wǎng)站建設(shè)公司,利通接單;提供成都網(wǎng)站建設(shè)、網(wǎng)站制作,網(wǎng)頁設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行利通網(wǎng)站開發(fā)網(wǎng)頁制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來合作!

那么,如果我想要達(dá)到類似 i++ 的這種效果,我應(yīng)該使用哪些集合或者說工具類呢?

在 JDK1.5 之前,為了確保在多線程下對某基本數(shù)據(jù)類型或者引用數(shù)據(jù)類型運(yùn)算的原子性,必須依賴于外部關(guān)鍵字 synchronized,但是這種情況在 JDK1.5 之后發(fā)生了改觀,當(dāng)然你依然可以使用 synchronized 來保證原子性,我們這里所說的一種線程安全的方式是原子性的工具類,比如 「AtomicInteger、AtomicBoolean」 等。這些原子類都是線程安全的工具類,他們同時也是 Lock-Free 的。下面我們就來一起認(rèn)識一下這些工具類以及 Lock - Free 是個什么概念。

了解 AtomicInteger

AtomicInteger 是 JDK1.5 新添加的工具類,我們首先來看一下它的繼承關(guān)系

與 int 的包裝類 Integer 一樣,都是繼承于 Number 類的。

這個 Number 類是基本數(shù)據(jù)類型的包裝類,一般和數(shù)據(jù)類型有關(guān)的對象都會繼承于 Number 類。

它的繼承體系很簡單,下面我們來看一下它的基本屬性和方法

AtomicInteger 的基本屬性

AtomicInteger 的基本屬性有三個

Unsafe 是 sun.misc 包下面的類,AtomicInteger 主要是依賴于 sun.misc.Unsafe 提供的一些 native 方法保證操作的原子性。

Unsafe 的 objectFieldOffset 方法可以獲取成員屬性在內(nèi)存中的地址相對于對象內(nèi)存地址的偏移量。說得簡單點(diǎn)就是找到這個變量在內(nèi)存中的地址,便于后續(xù)通過內(nèi)存地址直接進(jìn)行操作,這個值就是 value這個我們后面會再細(xì)說

value 就是 AtomicIneger 的值。

AtomicInteger 的構(gòu)造方法

繼續(xù)往下看,AtomicInteger 的構(gòu)造方法只有兩個,一個是無參數(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)行測試

 
 
 
 
  1. public class TAtomicTest implements Runnable{ 
  2.  
  3.     AtomicInteger atomicInteger = new AtomicInteger(); 
  4.  
  5.     @Override 
  6.     public void run() { 
  7.         for(int i = 0;i < 10000;i++){ 
  8.             System.out.println(atomicInteger.getAndIncrement()); 
  9.         } 
  10.     } 
  11.     public static void main(String[] args) { 
  12.  
  13.         TAtomicTest tAtomicTest = new TAtomicTest(); 
  14.  
  15.         Thread t1 = new Thread(tAtomicTest); 
  16.         Thread t2 = new Thread(tAtomicTest); 
  17.         t1.start(); 
  18.         t2.start(); 
  19.     } 
  20.  

通過輸出結(jié)果你會發(fā)現(xiàn)它是一個線程安全的操作,你可以修改 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)算。下面是測試代碼
 
 
 
 
  1. class TAtomicTestDecrement implements Runnable{ 
  2.  
  3.     AtomicInteger atomicInteger = new AtomicInteger(20000); 
  4.  
  5.     @Override 
  6.     public void run() { 
  7.         for(int i = 0;i < 10000 ;i++){ 
  8.             System.out.println(atomicInteger.getAndDecrement()); 
  9.         } 
  10.     } 
  11.  
  12.     public static void main(String[] args) { 
  13.  
  14.         TAtomicTestDecrement tAtomicTest = new TAtomicTestDecrement(); 
  15.  
  16.         Thread t1 = new Thread(tAtomicTest); 
  17.         Thread t2 = new Thread(tAtomicTest); 
  18.         t1.start(); 
  19.         t2.start(); 
  20.  
  21.     } 
  22.  

下面是 getAndDecrement 的示意圖

  • decrementAndGet:同樣的,decrementAndGet 方法就是先執(zhí)行遞減操作,然后再獲取 value 的值,示意圖如下

LazySet方法

volatile 有內(nèi)存屏障你知道嗎?

內(nèi)存屏障是啥啊?

內(nèi)存屏障,也稱內(nèi)存柵欄,內(nèi)存柵障,屏障指令等, 是一類同步屏障指令,是 CPU 或編譯器在對內(nèi)存隨機(jī)訪問的操作中的一個同步點(diǎn),使得此點(diǎn)之前的所有讀寫操作都執(zhí)行后才可以開始執(zhí)行此點(diǎn)之后的操作。也是一個讓CPU 處理單元中的內(nèi)存狀態(tài)對其它處理單元可見的一項(xiàng)技術(shù)。

CPU 使用了很多優(yōu)化,使用緩存、指令重排等,其最終的目的都是為了性能,也就是說,當(dāng)一個程序執(zhí)行時,只要最終的結(jié)果是一樣的,指令是否被重排并不重要。所以指令的執(zhí)行時序并不是順序執(zhí)行的,而是亂序執(zhí)行的,這就會帶來很多問題,這也促使著內(nèi)存屏障的出現(xiàn)。

語義上,內(nèi)存屏障之前的所有寫操作都要寫入內(nèi)存;內(nèi)存屏障之后的讀操作都可以獲得同步屏障之前的寫操作的結(jié)果。因此,對于敏感的程序塊,寫操作之后、讀操作之前可以插入內(nèi)存屏障。

內(nèi)存屏障的開銷非常輕量級,但是再小也是有開銷的,LazySet 的作用正是如此,它會以普通變量的形式來讀寫變量。

也可以說是:「懶得設(shè)置屏障了」

GetAndSet 方法

以原子方式設(shè)置為給定值并返回舊值。

它的源碼就是調(diào)用了一下 unsafe 中的 getAndSetInt 方法,如下所示

就是先進(jìn)行循環(huán),然后調(diào)用 getIntVolatile 方法,這個方法我在 cpp 中沒有找到,找到的小伙伴們記得及時告訴讓我學(xué)習(xí)一下。

循環(huán)直到 compareAndSwapInt 返回 false,這就說明使用 CAS 并沒有更新為新的值,所以 var5 返回的就是最新的內(nèi)存值。

CAS 方法

我們一直常說的 CAS 其實(shí)就是 CompareAndSet 方法,這個方法顧名思義,就是 「比較并更新」 的意思,當(dāng)然這是字面理解,字面理解有點(diǎn)偏差,其實(shí)人家的意思是先比較,如果滿足那么再進(jìn)行更新。

上面給出了 CAS Java 層面的源碼,JDK 官方給它的解釋就是 「如果當(dāng)前值等于 expect 的值,那么就以原子性的方式將當(dāng)前值設(shè)置為 update 給定值」,這個方法會返回一個 boolean 類型,如果是 true 就表示比較并更新成功,否則表示失敗。

CAS 同時也是一種無鎖并發(fā)機(jī)制,也稱為 Lock Free,所以你覺得 Lock Free 很高大上嗎?并沒有。

下面我們構(gòu)建一個加鎖解鎖的 CASLock

 
 
 
 
  1. class CASLock { 
  2.  
  3.     AtomicInteger atomicInteger = new AtomicInteger(); 
  4.     Thread currentThread = null; 
  5.  
  6.     public void tryLock() throws Exception{ 
  7.  
  8.         boolean isLock = atomicInteger.compareAndSet(0, 1); 
  9.         if(!isLock){ 
  10.             throw new Exception("加鎖失敗"); 
  11.         } 
  12.  
  13.         currentThread = Thread.currentThread(); 
  14.         System.out.println(currentThread + " tryLock"); 
  15.  
  16.     } 
  17.  
  18.     public void unlock() { 
  19.  
  20.         int lockValue = atomicInteger.get(); 
  21.         if(lockValue == 0){ 
  22.             return; 
  23.         } 
  24.         if(currentThread == Thread.currentThread()){ 
  25.             atomicInteger.compareAndSet(1,0); 
  26.             System.out.println(currentThread + " unlock"); 
  27.         } 
  28.     } 
  29.  
  30.     public static void main(String[] args) { 
  31.  
  32.         CASLock casLock = new CASLock(); 
  33.  
  34.         for(int i = 0;i < 5;i++){ 
  35.  
  36.             new Thread(() -> { 
  37.                 try { 
  38.                     casLock.tryLock(); 
  39.                     Thread.sleep(10000); 
  40.                 } catch (Exception e) { 
  41.                     e.printStackTrace(); 
  42.                 }finally { 
  43.                     casLock.unlock(); 
  44.                 } 
  45.             }).start(); 
  46.         } 
  47.  
  48.     } 

在上面的代碼中,我們構(gòu)建了一個 CASLock,在 tryLock 方法中,我們先使用 CAS 方法進(jìn)行更新,如果更新不成功則拋出異常,并把當(dāng)前線程設(shè)置為加鎖線程。在 unLock 方法中,我們先判斷當(dāng)前值是否為 0 ,如果是 0 就是我們愿意看到的結(jié)果,直接返回。否則是 1,則表示當(dāng)前線程還在加鎖,我們再來判斷一下當(dāng)前線程是否是加鎖線程,如果是則執(zhí)行解鎖操作。

那么我們上面提到的 compareAndSet,它其實(shí)可以解析為如下操作

 
 
 
 
  1. // 偽代碼 
  2.  
  3. // 當(dāng)前值 
  4. int v = 0; 
  5. int a = 0; 
  6. int b = 1; 
  7.  
  8. if(compare(0,0) == true){ 
  9.   set(0,1); 
  10. else{ 
  11.   // 繼續(xù)向下執(zhí)行 

也可以拿生活場景中的買票舉例子,你去景區(qū)旅游肯定要持票才能進(jìn),如果你拿著是假票或者不符合景區(qū)的票肯定是能夠被識別出來的,如果你沒有拿票拿你也肯定進(jìn)不去景區(qū)。

廢話少說,這就祭出來 compareAndSet 的示意圖

weakCompareAndSet: 媽的非常認(rèn)真看了好幾遍,發(fā)現(xiàn) JDK1.8 的這個方法和 compareAndSet 方法完全一摸一樣啊,坑我。。。

但是真的是這樣么?并不是,JDK 源碼很博大精深,才不會設(shè)計(jì)一個重復(fù)的方法,你想想 JDK 團(tuán)隊(duì)也不是會犯這種低級團(tuán)隊(duì),但是原因是什么呢?

《Java 高并發(fā)詳解》這本書給出了我們一個答案

AddAndGet

AddAndGet 和 getAndIncrement、getAndAdd、incrementAndGet 等等方法都是使用了 do ... while + CAS 操作,其實(shí)也就相當(dāng)于是一個自旋鎖,如果 CAS 修改成功就會一直循環(huán),修改失敗才會返回。示意圖如下

深入 AtomicInteger

我們上面探討了 AtomicInteger 的具體使用,同時我們知道 AtomicInteger 是依靠 volatile 和 CAS 來保證原子性的,那么我們下面就來分析一下為什么 CAS 能夠保證原子性,它的底層是什么?AtomicInteger 與樂觀鎖又有什么關(guān)系呢?

AtomicInteger 的底層實(shí)現(xiàn)原理我們再來瞧瞧這個可愛的 compareAndSetL(CAS) 方法,為什么就這兩行代碼就保證原子性了?

我們可以看到,這個 CAS 方法相當(dāng)于是調(diào)用了 unsafe 中的 compareAndSwapInt 方法,我們進(jìn)到 unsafe 方能發(fā)中看一下具體實(shí)現(xiàn)。

compareAndSwapInt 是 sun.misc 中的方法,這個方法是一個 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 方法,我們找到這個方法

C/C++ 源碼我也看不懂,但是這不妨礙我們找到關(guān)鍵代碼 Atomic::cmpxchg ,cmpxchg 是 x86 CPU 架構(gòu)的匯編指令,它的主要作用就是比較并交換操作數(shù)。我們繼續(xù)往下跟找一下這個指令的定義。

我們會發(fā)現(xiàn)對應(yīng)不同的 os,其底層實(shí)現(xiàn)方式不一樣

我們找到 Windows 的實(shí)現(xiàn)方式如下

我們繼續(xù)向下找,它其實(shí)定義的是第 216 行的代碼,我們找進(jìn)去

此時就需要匯編指令和寄存器相關(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 中代碼的大致意思就是

如果是多處理器的話就會執(zhí)行 lock,然后進(jìn)行比較操作。其中的 cmp 表示比較,mp 表示的就是 MultiProcess,je 表示相等跳轉(zhuǎn),L0 表示的是標(biāo)識位。

我們回到上面的匯編指令,我們可以看到,CAS 的底層就是 cmpxchg 指令。

樂觀鎖

你有沒有這個疑問,為什么 AtomicInteger 可以獲取當(dāng)前值,那為什么還會出現(xiàn) expectValue 和 value 不一致的情況呢?

因?yàn)?AtomicInteger 只是一個原子性的工具類,它不具有排他性,它不像是 synchronized 或者是 lock 一樣具有互斥和排他性,還記得 AtomicInteger 中有兩個方法 get 和 set 嗎?它們只是用 volatile 修飾了一下,而 volatile 不具有原子性,所以可能會存在 expectValue 和 value 的當(dāng)前值不一致的情況,因此可能會出現(xiàn)重復(fù)修改。

針對上面這種情況的解決辦法有兩種,一種是使用 synchronized 和 lock 等類似的加鎖機(jī)制,這種鎖具有獨(dú)占性,也就是說同一時刻只能有一個線程來進(jìn)行修改,這種方式能夠保證原子性,但是相對開銷比較大,這種鎖也叫做悲觀鎖。另外一種解決辦法是使用版本號或者是 CAS 方法。

「版本號」

版本號機(jī)制是在數(shù)據(jù)表中加上一個 version 字段來實(shí)現(xiàn)的,表示數(shù)據(jù)被修改的次數(shù),當(dāng)執(zhí)行寫操作并且寫入成功后,version = version + 1,當(dāng)線程 A 要更新數(shù)據(jù)時,在讀取數(shù)據(jù)的同時也會讀取 version 值,在提交更新時,若剛才讀取到的 version 值為當(dāng)前數(shù)據(jù)庫中的 version 值相等時才更新,否則重試更新操作,直到更新成功。

「CAS 方法」

還有一種方式就是 CAS 了,我們上面用了大量的篇幅來介紹 CAS 方法,那么我們認(rèn)為你現(xiàn)在已經(jīng)對其運(yùn)行機(jī)制有一定的了解了,我們就不再闡述它的運(yùn)行機(jī)制了。

任何事情都是有利也有弊,軟件行業(yè)沒有完美的解決方案只有最優(yōu)的解決方案,所以樂觀鎖也有它的弱點(diǎn)和缺陷,那就是 ABA 問題。

ABA 問題

ABA 問題說的是,如果一個變量第一次讀取的值是 A,準(zhǔn)備好需要對 A 進(jìn)行寫操作的時候,發(fā)現(xiàn)值還是 A,那么這種情況下,能認(rèn)為 A 的值沒有被改變過嗎?可以是由 A -> B -> A 的這種情況,但是 AtomicInteger 卻不會這么認(rèn)為,它只相信它看到的,它看到的是什么就是什么。舉個例子來說

假如現(xiàn)在有一個單鏈表,如下圖所示

A.next = B ,B.next = null,此時有兩個線程 T1 和 T2 分別從單鏈表中取出 A ,由于一些特殊原因,T2 把 A 改為 B ,然后又改為 A ,此時 T1 執(zhí)行 CAS 方法,發(fā)現(xiàn)單鏈表仍然是 A ,就會執(zhí)行 CAS 方法,雖然結(jié)果沒錯,但是這種操作會造成一些潛在的問題。

此時還是一個單鏈表,兩個線程 T1 和 T2 分別從單鏈表中取出 A ,然后 T1 把鏈表改為 ACD 如下圖所示

此時 T2,發(fā)現(xiàn)內(nèi)存值還是 A ,就會把 A 的值嘗試替換為 B ,因?yàn)?B 的引用是 null,此時就會造成 C、D 處于游離態(tài)

JDK 1.5 以后的 AtomicStampedReference類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當(dāng)前值是否等于預(yù)期值,判斷的標(biāo)準(zhǔn)就是當(dāng)前引用和郵戳分別和預(yù)期引用和郵戳相等,如果全部相等,則以原子方式設(shè)置為給定的更新值。

好了,上面就是 Java 代碼流程了,看到 native 我們知道又要擼 cpp 了。開擼

簡單解釋一下就是 UnsafeWrapper 就是包裝器,換個名字而已。然后經(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)換。

后記

拋出來一個問題,CAS 能保證變量之間的可見性么?為什么?

還有一個問題,getIntVolatile 方法的 cpp 源碼在哪里?怎么找?

 本文轉(zhuǎn)載自微信公眾號「Java建設(shè)者」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系Java建設(shè)者公眾號。


文章名稱:告訴你一個 AtomicInteger 的驚天大秘密!
文章源于:http://www.5511xx.com/article/dppeigo.html