您好,登錄后才能下訂單哦!
這篇文章主要講解了“java多線程CAS的介紹”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“java多線程CAS的介紹”吧!
1 CAS講解
在工作中,我們往往需要面對多線程計數的問題,我們第一反應,使用“synchronized”,控制并發。
@Slf4j public class CASDemo extends Thread{ private static int t = 0; @Override public void run() { increment(); log.info("-----------{}------------",t); } private synchronized static void increment() { t++; } public static void main(String[] args){ for (int i = 0 ; i < 100 ; i++){ CASDemo casDemo = new CASDemo(); casDemo.start(); } }
結果如下:
從上面結果來看符合我們的預期,100個線程執行了1次,最后t為100。但是這是最好的實現機制嗎?我們知道“synchronized”是同步鎖,即使jdk6以后已經對其進行了優化(具體可以見另一篇文章:java多線程基礎知識之synchronized原理分析),只是用來計數,是否太大材小用了。有沒有一種更加優雅的解決方案?
@Slf4j public class CASDemo extends Thread{ private static AtomicInteger t = new AtomicInteger(0); @Override public void run() { log.info("-----------{}------------",t.incrementAndGet()); } public static void main(String[] args){ for (int i = 0 ; i < 100 ; i++){ CASDemo casDemo = new CASDemo(); casDemo.start(); } } }
結果如下:
從結果上來看,第一種和第二種結果是一樣的。為啥推薦第一種方法呢?因為第二種使用了無鎖式的“compareAndSwap”即“CAS”,既然“CAS”是無鎖的,那么是怎么樣保證其實線程安全的。
“compareAndSwap”從字面上看,這一過程肯定包含了比較和替換兩個動作,具體步驟如下:
(1)線程從內存中讀取 i 的值,假如此時 i 的值為 0,我們把這個值稱為 k 吧,即此時 k = 0。
(2)令 j = k + 1。
(3)用 k 的值與內存中i的值相比,如果相等,這意味著沒有其他線程修改過 i 的值,我們就把 j(此時為1) 的值寫入內存;如果不相等(意味著i的值被其他線程修改過),我們就不把j的值寫入內存,而是重新跳回步驟 1,繼續這三個操作。具體源碼如下:
同時,整個"CAS"是原子的,對應操作系統的一條硬件操作指令,盡管看似有很多操作在里面,但操作系統能夠保證它是原子執行的。
通過上面的流程講解,我們可以發現其不可能從內存中同時取到相同的K值,并且分別+1然后提交到內存中,從而保證的線程安全。
但是我們仍然面臨一個問題:誰偷偷更改了我的值。
舉個例子,當線程A即將要執行第三步的時候,線程 B 把 i 的值加1,之后又馬上把 i 的值減 1,然后,線程 A 執行第三步,這個時候線程 A 是認為并沒有人修改過 i 的值,因為 i 的值并沒有發生改變。而這,就是我們平常說的ABA問題。對于基本類型的值來說,這種把數字改變了在改回原來的值是沒有太大影響的,但如果是對于引用類型的話,就會產生很大的影響了。
怎么解決這個問題呢?——版本控制(參考樂觀鎖)。
例如,每次有線程修改了引用的值,就會進行版本的更新,雖然兩個線程持有相同的引用,但他們的版本不同,這樣,我們就可以預防 ABA 問題了。Java 中提供了 AtomicStampedReference 這個類,就可以進行版本控制了。
給個demo.
//構造方法, 傳入引用和戳 public AtomicStampedReference(V initialRef, int initialStamp) //返回引用 public V getReference() //返回版本戳 public int getStamp() //如果當前引用 等于 預期值并且 當前版本戳等于預期版本戳, 將更新新的引用和新的版本戳到內存 public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) //如果當前引用 等于 預期引用, 將更新新的版本戳到內存 public boolean attemptStamp(V expectedReference, int newStamp) //設置當前引用的新引用和版本戳 public void set(V newReference, int newStamp)
public static void main(String[] args) { String str1 = "aaa"; String str2 = "bbb"; AtomicStampedReference<String> reference = new AtomicStampedReference<String>(str1,1); reference.compareAndSet(str1,str2,reference.getStamp(),reference.getStamp()+1); System.out.println("reference.getReference() = " + reference.getReference()); boolean b = reference.attemptStamp(str2, reference.getStamp() + 1); System.out.println("b: "+b); System.out.println("reference.getStamp() = "+reference.getStamp()); boolean c = reference.weakCompareAndSet(str2,"ccc",4, reference.getStamp()+1); System.out.println("reference.getReference() = "+reference.getReference()); System.out.println("c = " + c); } 輸出: reference.getReference() = bbb b: true reference.getStamp() = 3 reference.getReference() = bbb c = false c為什么輸出false呢, 因為版本戳不一致啦
2jdk8對CAS的優化
由于采用這種 CAS 機制是沒有對方法進行加鎖的,所以,所有的線程都可以進入 increment() 這個方法,假如進入這個方法的線程太多,就會出現一個問題:每次有線程要執行第三個步驟的時候,i 的值老是被修改了,所以線程又到回到第一步繼續重頭再來。
而這就會導致一個問題:由于線程太密集了,太多人想要修改 i 的值了,進而大部分人都會修改不成功,白白著在那里循環消耗資源。
為了解決這個問題,Java8 引入了一個 cell[] 數組,它的工作機制是這樣的:假如有 5 個線程要對 i 進行自增操作,由于 5 個線程的話,不是很多,起沖突的幾率較小,那就讓他們按照以往正常的那樣,采用 CAS 來自增吧。但是,如果有 100 個線程要對 i 進行自增操作的話,這個時候,沖突就會大大增加,系統就會把這些線程分配到不同的 cell 數組元素去,假如 cell[10] 有 10 個元素吧,且元素的初始化值為 0,那么系統就會把 100 個線程分成 10 組,每一組對 cell 數組其中的一個元素做自增操作,這樣到最后,cell 數組 10 個元素的值都為 10,系統在把這 10 個元素的值進行匯總,進而得到 100,二這,就等價于 100 個線程對 i 進行了 100 次自增操作。
總之,jdk8對于高并發的情況下,采用了類似減少鎖粒度方法來提高性能。
感謝各位的閱讀,以上就是“java多線程CAS的介紹”的內容了,經過本文的學習后,相信大家對java多線程CAS的介紹這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。