代碼小哥都督 2021-09-20 03:35:46 阅读数:129
[](
)原子操作類簡介
在並發編程中很容易出現並發安全的問題,有一個很簡單的例子就是多線程更新變量i=1,比如多個線程執行i++操作,就有可能獲取不到正確的值,而這個問題,最常用的方法是通過Synchronized進行控制來達到線程安全的目的([關於synchronized可以看這篇文章](
))。但是由於synchronized是采用的是悲觀鎖策略,並不是特別高效的一種解决方案。實際上,在J.U.C下的atomic包提供了一系列的操作簡單,性能高效,並能保證線程安全的類去更新基本類型變量,數組元素,引用類型以及更新對象中的字段類型。atomic包下的這些類都是采用的是樂觀鎖策略去原子更新數據,在java中則是使用CAS操作具體實現。
[](
)預備知識-CAS操作
能够弄懂atomic包下這些原子操作類的實現原理,就要先明白什麼是CAS操作。
什麼是CAS?
使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設每一次執行臨界區代碼都會產生沖突,所以當前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖。而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略,它假設所有線程訪問共享資源的時候不會出現沖突,既然不會出現沖突自然而然就不會阻塞其他線程的操作。因此,線程就不會出現阻塞停頓的狀態。那麼,如果出現沖突了怎麼辦?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑒別線程是否出現沖突,出現沖突就重試當前操作直到沒有沖突為止。
CAS的操作過程
CAS比較交換的過程可以通俗的理解為CAS(V,O,N),包含三個值分別為:V 內存地址存放的實際值;O 預期的值(舊值);N 更新的新值。當V和O相同時,也就是說舊值和內存中實際的值相同錶明該值沒有被其他線程更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,錶明該值已經被其他線程改過了,則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個線程使用CAS操作一個變量時,只有一個線程會成功,並成功更新,其餘會失敗。失敗的線程會重新嘗試,當然也可以選擇掛起線程。
CAS的實現需要硬件指令集的支撐,在JDK1.5後虛擬機才可以使用處理器提供的CMPXCHG指令實現。
Synchronized VS CAS
元老級的Synchronized(未優化前)最主要的問題是:在存在線程競爭的情况下會出現線程阻塞和喚醒鎖帶來的性能問題,因為這是一種互斥同步(阻塞同步)。而CAS並不是武斷的將線程掛起,當CAS操作失敗後會進行一定的嘗試,而非進行耗時的掛起喚醒的操作,因此也叫做非阻塞同步。這是兩者主要的區別。
CAS的問題
ABA問題
因為CAS會檢查舊值有沒有變化,這裏存在這樣一個有意思的問題。比如一個舊值A變為了成B,然後再變成A,剛好在做CAS時檢查發現舊值並沒有變化依然為A,但是實際上的確發生了變化。解决方案可以沿襲數據庫中常用的樂觀鎖方式,添加一個版本號可以解决。原來的變化路徑A->B->A就變成了1A->2B->3C。
自旋時間過長
使用CAS時非阻塞同步,也就是說不會將線程掛起,會自旋(無非就是一個死循環)進行下一次嘗試,如果這裏自旋時間過長對性能是很大的消耗。如果JVM能支持處理器提供的pause指令,那麼在效率上會有一定的提昇。
[](
)原子更新基本類型
atomic包提高原子更新基本類型的工具類,主要有這些:
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;
這幾個類的用法基本一致,這裏以AtomicInteger為例總結常用的方法
addAndGet(int delta) :以原子方式將輸入的數值與實例中原本的值相加,並返回最後的結果;
incrementAndGet() :以原子的方式將實例中的原值進行加1操作,並返回最終相加後的結果;
getAndSet(int newValue):將實例中的值更新為新值,並返回舊值;
getAndIncrement():以原子的方式將實例中的原值加1,返回的是自增前的舊值;
還有一些方法,可以查看API,不再贅述。為了能够弄懂AtomicInteger的實現原理,以getAndIncrement方法為例,來看下源碼:
可以看出,該方法實際上是調用了unsafe實例的getAndAddInt方法,unsafe實例的獲取時通過UnSafe類的靜態方法getUnsafe獲取:
Unsafe類在sun.misc包下,Unsafer類提供了一些底層操作,atomic包下的原子操作類的也主要是通過Unsafe類提供的compareAndSwapInt,compareAndSwapLong等一系列提供CAS操作的方法來進行實現。下面用一個簡單的例子來說明AtomicInteger的用法:
輸出結果
例子很簡單,就是新建了一個atomicInteger對象,而atomicInteger的構造方法也就是傳入一個基本類型數據即可,對其進行了封裝。對基本變量的操作比如自增,自减,相加,更新等操作,atomicInteger也提供了相應的方法進行這些操作。但是,因為atomicInteger借助了UnSafe提供的CAS操作能够保證數據更新的時候是線程安全的,並且由於CAS是采用樂觀鎖策略,因此,這種數據更新的方法也具有高效性。
AtomicLong的實現原理和AtomicInteger一致,只不過一個針對的是long變量,一個針對的是int變量。而boolean變量的更新類AtomicBoolean類是怎樣實現更新的呢?核心方法是compareAndSet
t方法,其源碼如下:
可以看出,compareAndSet方法的實際上也是先轉換成0,1的整型變量,然後是通過針對int型變量的原子更新方法compareAndSwapInt來實現的。可以看出atomic包中只提供了對boolean,int ,long這三種基本類型的原子更新的方法,參考對boolean更新的方式,原子更新char,doule,float也可以采用類似的思路進行實現。
[](
)原子更新數組類型
atomic包下提供能原子更新數組中元素的類有:
AtomicIntegerArray:原子更新整型數組中的元素;
AtomicLongArray:原子更新長整型數組中的元素;
AtomicReferenceArray:原子更新引用類型數組中的元素
這幾個類的用法一致,就以AtomicIntegerArray來總結下常用的方法:
addAndGet(int i, int delta):以原子更新的方式將數組中索引為i的元素與輸入值相加;
getAndIncrement(int i):以原子更新的方式將數組中索引為i的元素自增加1;
compareAndSet(int i, int expect, int update):將數組中索引為i的比特置的元素進行更新
可以看出,AtomicIntegerArray與AtomicInteger的方法基本一致,只不過在AtomicIntegerArray的方法中會多一個指定數組索引比特i。下面舉一個簡單的例子:
版权声明:本文为[代碼小哥都督]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/09/20210920033545867S.html