文章前記
程序員工作久了便可能整日忙碌于“增刪改查”中,迷失方向,毫無進步。
該公眾號致力于分享軟件開發(fā)相關(guān)的原創(chuàng)干貨,助你完成從程序員到架構(gòu)師的進階之路!
努力!做一個NB的Coder!
1??背景
之前的文章中我們已經(jīng)講過,Java的AtomicInteger類中能夠?qū)⒆x和寫封裝成為一個原子操作,例如其中的getAndIncrement()方法就可以實現(xiàn)原子化的i++操作。
這一切的實現(xiàn)是通過系統(tǒng)原生的CAS操作實現(xiàn)的。
CAS操作即比較并交換操作,能夠在內(nèi)存真值與預(yù)期原值一樣時,將新值放入指定的內(nèi)存中。
本文我們探討基于CAS操作實現(xiàn)的讀寫原子化中引發(fā)的問題。
2??CAS存在的問題
CAS實現(xiàn)了高效的原子操作,但是仍然存在一些問題,主要有三個:
1 ABA問題
2 循環(huán)開銷問題
3 無法應(yīng)用與多個共享變量
2.1? ABA問題
因為CAS需要在操作值的時候檢查下值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發(fā)現(xiàn)它的值沒有發(fā)生變化,認為情況還是樂觀的。此時可能引發(fā)錯誤。
例如,時刻t1在隊列中CAS操作前獲取預(yù)期原值A(chǔ)=5,之后隊列發(fā)生了移動,再次獲取的原值仍然為5,此時進行了CAS操作。則會引發(fā)錯誤。
ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。
Java1.5開始,atomic包中存在一個AtomicStampedReference類來解決ABA問題。
該類的compareAndSet方法會先檢查當(dāng)前引用是否發(fā)生變化,如果沒有變化才會使用CAS更新其值。
源代碼如下:
2.2? 循環(huán)開銷問題
因為CAS是基于樂觀鎖的思想的,需要不斷判斷樂觀情況是否成立,因此是一個循環(huán)操作,常被稱為CAS自旋。如果并發(fā)嚴重,則CAS自旋會不斷嘗試,導(dǎo)致CPU開銷大。
解決此問題的辦法是在多次CAS操作失敗時,能夠暫停一段時間,在進行CAS操作。防止在其他線程密集修改某變量時對該變量不斷進行CAS自旋。當(dāng)然,這需要JVM的支持。
2.3? 無法應(yīng)用于多個共享變量
對一個共享變量進行CAS操作時可以的,那如果對一組變量展開操作呢?顯然是不可以的,因為內(nèi)存位置V是一個值,而非一組值。這個時候只能使用鎖。
其實,還有一個辦法,即將這一組變量封裝成一個對象,從而將對一組變量的操作轉(zhuǎn)化為對一個對象的操作。
Java1.5之后,便可以使用AtomicReference來封裝一個需要原子化更新的對象。
3?總結(jié)
雖然,AtomicInteger類中的原子化操作存在一些問題,但是它與加同步鎖的方法相比仍然在性能、易用性上具有巨大的優(yōu)勢。
希望大家能夠在日常的編碼中掌握并使用AtomicInteger類中的相關(guān)方法,寫出簡潔、高效的代碼。
—END—
微信公眾號:程序員進階架構(gòu)師
分享讓你從程序員進階架構(gòu)師的原創(chuàng)干貨!
歡迎關(guān)注我們,不錯過每期的原創(chuàng)干貨!