Java併發之原子變量及CAS算法-下篇
概述
本文主要講在Java併發編程的時候,如果保證變量的原子性,在JDK提供的類中是怎麼保證變量原子性的呢?。對應Java中的包是:java.util.concurrent.atomic包下。因為涉及到了CAS算法,需要對CAS算法講解及CAS算法三個問題怎麼解決以及和Synchroized比較。文章比較長,所以就分為上下兩個篇幅講解。本文是上篇《Java併發之原子變量及CAS算法-下篇》
本文是《凱哥分享Java併發編程之J.U.C包講解》系列教程中的第四篇。如果想系統學習,凱哥(kaigejava)建議從第一篇開始看。
在上一篇中,我們講解了i++在多線程下變量原子性問題以及怎麼解決。在本篇中,我們詳細講解什麼是CAS算法?CAS和Synchroized區別是什麼?以及CAS算法產生的問題及怎麼解決。
CAS簡介
什麼是CAS算法?
CAS:Compare-And-Swap即比較並交換的意思。
CAS包含了三個操作的數據:
主內存中的變量值:V
預估值(可以理解為原來舊的值):A
更新值(操作後,要更新的值):B
CAS的特點:
當且僅當預估值A=內存值V的時候,才會將V的值更新為B。否則也不操作。
V==A;V=B;
使用CAS算法多線程操作的時候,有且僅有一個線程可以操作成功,其他線程都會操作失敗。失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
失敗的線程採用自旋來進行嘗試的。
我們以AtomicInteger對象中的getAndIncrement()方法為例來看看:
模擬後的源碼:
CAS VS Synchroized比較
那麼CAS的算法的效率為什麼會比Synchronized的效率高呢?
Synchroized是阻塞算法的;而CAS是非阻塞的,採用的是樂觀鎖技術。
因為阻塞算法是CPU切換的,而CAS是CPU指令操作。CPU切換時間相對於CPU指令操作來說時間更長。所以使用CAS算法的線程比使用Synchroized的效率高。
CAS的優缺點
優點:一種線程同步的解決方案,使用CAS就可以不用加鎖來實現線程的安全性。
缺點:
1:只能保證一個共享變量的原子操作;
2:循環時間長,開銷很大;
3:會產生ABA問題。
缺點解決方案:
缺點一:
當對一個共享變量操作的時候,可以使用帶有自旋(循環)的CAS方法來保證原子性操作,但是如果是多個變量共享的時候,可以封裝到對象中或者是使用鎖來保證原子性。
缺點二:
如果採用自旋的CAS方式來保證原子性,會一直進行嘗試。如果時間太長的話,對CPU來說也會帶來很大開銷的。
缺點三:ABA問題
何為ABA問題?
如線程A修改共享變量值為A;線程B修改值為B。後來共享變量有被修改成了A,這種情況下CAS算法操作就會誤認為共享變量A沒有別修改過。這就是CAS算法的“漏洞”。
舉個很簡單的例子:
解決ABA問題
看到這裡大家或許心裡會想,我Kao,這不就是一個坑嗎?JDK埋下的坑!既然有這個坑,那還敢用嗎??淡定,保持淡定點。你能想到的問題,JDK開發者也能想到。所以補救辦法就是:
注意:是AtomicStampedReference。雖然和AtomicReference這個類有點像。但是不一樣。
查看源碼註釋:
簡單理解,就是這個類添加了一個版本號。,每次操作都對版本號進行自增,那每次CAS不僅要比較value,還要比較stamp,當且僅當兩者都相等,才能夠進行更新。
具體怎麼操作的呢?
在初始化的時候,就定義了pair對象。
在compareAndSet的時候,會對版本號進行比較。如下圖:
講明白了CAS原理之後,我們來修改i++的問題。使其成為保證原子性:
很簡單隻需要修改兩行代碼即可:
1:聲明變量的時候使用AtomicInteger對象:
private AtomicInteger shardData = new AtomicInteger(0);
new AtomicInteger(0)其中的0可以不用寫
2:修改i++的方法:
return shardData.getAndIncrement();
這樣就可以了。
總結
Java中保證變量原子性使用的是current.atomic包下的對象來實現的。
如何保證原子性呢?
1:變量都是用volatile關鍵字修飾後,保證了內存的可見性;
2:使用CAS算法,保證了原子性。
Synchroized VS volatile VS CAS
在上一篇文章中我們知道了Volatile只能保證變量共享變量在內存中的可見性;不互斥;不能保證原子性;
在本篇中,我們知道了CAS是非阻塞的使用樂觀鎖技術來實現原子性。但是會產生其他問題,不過也可以解決。
Synchroized是阻塞性算法的實現。具有互斥性