上篇文章有說過 多線程環(huán)境下 進行變量屬性 自增操作時會造成線程不安全的情況郑气,也有說到 volatile 關鍵字,最后也不能保證線程安全腰池,因為多線程情況下 他不能保證原子性,不能保證寫操作過程不可以被插隊,最后有提到java.util.current.atomic包中的AtomicInteger類,那么它是如何實現(xiàn)線程安全的呢?尾组,讓我們一探究竟!
?
atomc包是java專門提供保證原子性的包,里邊提供了基本類型的原子操作類示弓,天生就是保證變量原子性的讳侨。
?
今天我們就借此先來說一下AtomicInteger,其他類型類的方法 實現(xiàn)方式都一樣奏属。
先對比一下沒有使用前會引發(fā)的狀況:
?
?
?
可以看到沒有達到預想的效果跨跨,并且每次產生的結果都不一樣,這就是上篇
文章所說到的,沒有保證原子性勇婴,在執(zhí)行+1操作時被其他線程插隊忱嘹,導致每次往主內存寫入了相同的值。注:加上volatile也是會產生一樣的結果耕渴!因為volatile不能保證原子性拘悦。
接下來我們使用AtomicInteger來試一下:
?
?
可以看到,達到了我們預期的效果橱脸。 那么他到底是是如何實現(xiàn)的呢础米? 我們來一探究竟!
在查看究竟前先講解一個它的一個方法,以及涉及到的知識點,以便于后邊的理解:
先說個點:CAS ==> Compare and Swap ==> 比較且交換
接下來 簡單使用以下AtomicInteger提供的一個方法:
?
expert:期望值添诉,即 期望改變的值
update:更改值椭盏,即 將期望值更改為什么
這里第一次我期望將初始值1更改為2,操作完成后 我再次期望將1 更改為2吻商,我們看下執(zhí)行結果:
?
第一次更改 成功 為 true 值變成了2掏颊,第二次執(zhí)行失敗 false 里邊值還是2,第二次沒有被更改過艾帐。這就是所謂的比較交換乌叶。
我們看下這個方法里邊的實現(xiàn):
?
this , valueOffset 下邊會說是什么意思, expect , update 即是期望值和更改值上邊有說
記住compareAndSwapInt()這個方法柒爸,CAS實現(xiàn)的 關鍵方法
我們先將目光轉到自增方法getAndIncrement方法上看看底層如何實現(xiàn):
?
解釋下 這個 方法存在的內容:
unsafe:
由于Java方法無法直接訪問底層系統(tǒng)准浴,需要通過本地(native)方法來訪問,Unsafe相當于一個后門捎稚,基于該類可以直接操作特定內存的數(shù)據(jù)乐横。Unsafe類存在于sun.misc包中,其內部方法操作可以像C的指針一樣直接操作內存今野,因為Java中CAS操作的執(zhí)行依賴于Unsafe類的方法葡公。
this:
表示當前AtomicInteger類。
valueOffset:
用一個圖片說明条霜,在AtomicInteger類有聲明:
?
最后屬性 1 是要增加的數(shù)值 這里是1催什。
讓我們點進去再看看:
?
上邊我們有看到 compareAndSwapInt 這個方法我們沒有細說,這里說下:
解釋下:
var1: 操作的對象
var2: 內存偏移量地址
var4:要增加的值
var5: 根據(jù)內存偏移量地址獲取到的值 (上邊提到的期望值)
var5 + var4 : 更改為的值
然后這里是個循環(huán)宰睡,先獲取 當前內存偏移量位置 的屬性值作為期望值蒲凶,然后進行修改,如果過程其他線程已經改完了拆内,那么修改返回值為false旋圆,則繼續(xù)循環(huán)重新獲取期望值,再次進行更改麸恍,直到修改成為止才退出循環(huán)灵巧。
1.根據(jù)傳入對象和內存偏移量地址 拿取對應位置最新的值,為期望值
2.進行寫操作,如果過程被其他線程更改孩等,則期望值就會配對不上就會修改失敗艾君,繼續(xù)循環(huán)直到成功。
可能 會有人問 這樣操作進行修改過程中不會被打斷嗎肄方?
對是的冰垄,不會被打斷的,上邊又說Unsafe類中的方法是可以直接訪問計算機內存的权她,可以跟c語言一樣虹茶。
這是在網上找的代碼,內部在向CPU發(fā)送CAS指令時的匯編指令,是一條CPU并發(fā)原語隅要,過程是原子的蝴罪。
CAS并發(fā)語體現(xiàn)在JAVA語言中就是sun.misc.Unsafe類中的各個方法。調用UnSafe類中的CAS方法步清,JVM會幫我們實現(xiàn)出CAS匯編指令要门,這是一種完全依賴于硬件的功能,通過它實現(xiàn)了原子操作廓啊。再次強調欢搜,由于CAS是一種系統(tǒng)原語袋励,原語屬于操作系統(tǒng)用范疇鹏氧,是由若干條指令組成的,用于完成某個功能的一個過程斩郎,并且原語的執(zhí)行必須是連續(xù)的第步,在執(zhí)行過程中不允許被中斷疮装,也就是說CAS是一條CPU的原子指令,不會造成所謂的數(shù)據(jù)不一致問題粘都。所以執(zhí)行過程是不會被打斷的廓推,是線程安全的。
但是 會引發(fā)出來另一個問題切記: ABA問題
什么是ABA:
class ABA{
int i = 1;
}
假設此時有兩條線程 操作i這個變量
線程1驯杜,2 同時啟動 進行 CAS 操作受啥,他們讀到的期望值都為 1
接下來 兩個線程執(zhí)行以下過程:
線程1: 將 1 更改為 2 做个,然后再將 2 更改為1
線程2:休息 5秒鐘鸽心,將 1 更改為2
線程1 肯定比 線程2 先執(zhí)行完,線程2 執(zhí)行的時候是可以成功將1 更改為2的居暖,但是有一個問題顽频,在他更改的時候他不知道線程1 已經進行了多次更改,將1變?yōu)?又變?yōu)榱?太闺。
就像我桌子上的水被偷喝了糯景,然后喝完又給我接了一杯,而我回來后卻不知道已經被他人喝過了被他人占了個便宜。有種偷天換月的意思蟀淮。
解釋完之后給大家上個理論知識點:
CAS算法實現(xiàn)一個重要前提需要去除內存中某時刻的數(shù)據(jù)并立刻比較并替換最住,那么在這個時間出現(xiàn)時間差類會導致數(shù)據(jù)變化。
這就是很典型的ABA問題怠惶,那么如何解決呢涨缚?
可以加時間戳,版本號都可以解決:
AtomicStampedReference類:
上邊初始化了 值為10 版本號為1的一個 AtomicStampedReference類策治,可以看到同樣再調用compareAndSet方法的時候需要傳4個值:
分別為 期望值脓魏,修改值,期望版本號通惫,修改版本號
有了版本號就可以避免CAS出現(xiàn)ABA的問題茂翔。
Atomic包里邊不只是只有 Integer,Long等基本類型的原子類哦,自定義類同樣可以原子操作:
可以通過AtomicReference類來操作
大家可以試試下邊代碼有時間的話:
?
總結一下:
為什么明明可以在 自增方法添加一個Synchronized關鍵字就可以解決為什么要通過原子類的CAS來解決履腋。
Synchronized的比較笨重在上方例子珊燎,沒必要殺雞用牛刀,剛好也可以借助上方例子說一下CAS,他在使用時會將它修飾的代碼塊給鎖住遵湖,其他線程不可以訪問俐末,會大大降低并發(fā)。
CAS 則可以大大提升并發(fā)奄侠,線程都可以同時執(zhí)行卓箫,只不過是修改成功與否的問題了。
當然垄潮,這里說CAS也比較多也說一下它的缺點:
CAS雖然可以提升并發(fā)量烹卒,但容易給CPU造成很大的開銷,并且也只能保證一個共享變量的原子性弯洗,對多個共享變量不能同時原子性旅急。