學習過多線程的同窗必定看到過 CAS
這個概念,CAS
是 Compare-and-swap
的簡稱,那它有什麼做用呢 ?爲何可以代替 synchorinzed
?java
CAS(Compare-and-swap)
:比較和替換,它是設計併發算法是的一種經常使用技術。使用 CAS
操做可用於保證變量更新的原子性。算法
CAS
操做涉及到 3
個值:安全
V
當前值:變量當前在內存中的值A
指望值:指望變量當前在內存中的值B
更新值:準備爲變量賦予的新值CAS
操做邏輯以下:CAS
比較 V
和 A
的值,若是值相等則變量值更新爲 B
,不然不進行任何操做。以下圖:markdown
若是第一次接觸到 CAS
的概念可能對它的操做邏輯產生疑惑,爲何在相等的狀況下執行賦值操做而不等的狀況下反而什麼都不作呢?下面看一個自增操做的示例:多線程
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
//獲取當前值, 此示例中當前值也是本次 `CAS` 的指望值
int current = get();
//獲取更新值
int next = current + 1;
//執行CAS操做
if (compareAndSet(current, next)) {
//成功後纔會返回指望值,不然無線循環
return next;
}
}
}
複製代碼
如今有線程 A、B
,當線程 A
執行到 CAS
操做, 獲取當前值、指望值和更新值分別爲 0、0、1
, 此時線程 A
被掛起,線程 B
進入執行 CAS
操做將變量值成功更新爲 1
, 線程 A
繼續執行 CAS
操做, 因爲此時變量當前值已經被修改,因此本次 CAS
執行失敗,循環繼續執行 CAS
自增操做,執行成功退出循環。併發
經過上面的示例知道 CAS
能夠保證變量更新的原子性,進而能夠聯想到 volatile
關鍵字的功能缺陷。ide
先來看 volatile
關鍵字的做用,以下:高併發
volatile
關鍵字的缺陷正是其沒法保證變量操做的原子性,好比單目運算符 ++、--
就涉及到讀寫兩個操做。因此常常能夠看到 volatile
和 synchorinzed
關鍵字共用的場景以保證變量操做的原子性,而 CAS
也能夠保證變量操做的原子性。那 CAS
是否能夠替代 synchorinzed
呢?在某些狀況下是能夠的。性能
好比在上面的示例中,AtomicInteger().getAndIncrement()
的內部源碼以下:學習
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
複製代碼
能夠看到這裏就是經過 volatile + CAS
的操做來保證變量操做安全性的。
那下面對 CAS
和 synchorinzed
的使用作下對比,主要從以下兩方面:
功能限制:
CAS
更加輕量級,synchorinzed
升級爲重量鎖時會影響系統性能;CAS
僅能保證單個變量操做的原子性,synchorinzed
能夠保證代碼塊內全部變量操做的原子性。併發規模:
CAS
更具優點,synchorinzed
在少許狀況下仍可能升級爲重量鎖影響系統性能。synchorinzed
更具優點,因爲 CAS
的不少實現都會使用了自旋操做(以下文將介紹的 Atomic***
系列),當在大量線程的狀況下 CAS
會頻繁執行失敗進而須要頻繁重試,這樣會浪費 CPU
資源。結論: 在少許線程且僅需保證單個變量線程安全的狀況下可以使用 volatile + CAS
替代 volatile + synchorinzed
。
注意:
volatile + CAS
和volatile + synchorinzed
使用時要理解它們各自的角色和起到的做用:
volatile
:保證有序性和可見性;CAS
、synchorinzed
:保證操做原子性。
注意:多線程環境下正確使用
CAS
必須搭配volatile
關鍵字。由於CAS
雖然能夠保證原子性,但其沒法保證變量在不一樣線程內存空間的安全性,因此須要volatile
來保證變量更新對於不一樣線程是可見的。
除了上文提到過 CAS
的兩個缺點:
CAS
一直失敗會一直重試,浪費 CPU
資源針。對這個問題的一個思路是引入退出機制,如重試次數超過必定閾值後失敗退出。固然,更重要的是避免在高併發環境下使用 CAS
。還有一個問題就是 ABA
問題,現有線程 A、B
:
1
讀取內存中的數據爲 A
;2
修改內存數據爲 B
;2
修改內存數據爲 A
;1
對數據執行 CAS
操做。因爲執行到第四步時內存數據仍然爲 A
,但其實數據已經被修改過了。這就是 ABA
問題。針對 ABA
問題能夠經過引入版本號的方式解決,每次修改內存中的值版本號都 +1
,在執行 CAS
操做是不只比較內存中的值也比較版本號,只有二者都相同時才執行成功。Java
中提供的 java.util.concurrent.atomic.AtomicStampedReference
也是經過版本號來解決 ABA
問題的。
在 Android
中咱們也能夠經過 Atomic***
類來使用 volatile + CAS
。
AtomicFile
AtomicInteger
AtomicLong
AtomicBoolean
AtomicReferenceFieldUpdater
AtomicStampedReference
前幾個比較好理解,分別能夠保證 file、int、long、boolean
類型數據的原子操做,那麼若是操做數據爲 String
或類型不可知怎麼辦呢?這時候就可使用 AtomicReferenceFieldUpdater()
了。在 Kotlin.lazy
的實現中就使用到了 AtomicReferenceFieldUpdater
:
private class SafePublicationLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
@Volatile private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// this final field is required to enable safe initialization of the constructed instance
private val final: Any = UNINITIALIZED_VALUE
override val value: T
get() {
val value = _value
if (value !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return value as T
}
val initializerValue = initializer
// if we see null in initializer here, it means that the value is already set by another thread
if (initializerValue != null) {
val newValue = initializerValue()
if (valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)) {
initializer = null
return newValue
}
}
@Suppress("UNCHECKED_CAST")
return _value as T
}
......
companion object {
private val valueUpdater = java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(
SafePublicationLazyImpl::class.java,
Any::class.java,
"_value"
)
}
}
複製代碼
AtomicReferenceFieldUpdater
經過靜態方法 newUpdater()
獲取實例對象。newUpdater()
方法有三個參數,分別是:
tclass
:目標變量所在類的 class
對象;vclass
:目標變量的類型 class
對象;fieldName
:目標變量名。在上述 kotlin.lazy
源碼中經過比較初始值,保證在多線程環境中僅第一次賦值有效:
valueUpdater.compareAndSet(this, UNINITIALIZED_VALUE, newValue)
複製代碼
再來看 AtomicStampedReference
,它的初始化方法以下:
public AtomicStampedReference(V initialRef, int initialStamp) {
}
複製代碼
能夠看到初始化方法須要兩個參數:
用法以下:
private val atomicObj: AtomicStampedReference<String> = AtomicStampedReference("A", 0)
val t1 = Thread {
try {
TimeUnit.SECONDS.sleep(1)
} catch (e: InterruptedException) {
}
println("run thread1")
atomicObj.compareAndSet("A", "B", atomicObj.stamp, atomicObj.stamp + 1)
atomicObj.compareAndSet("B", "A", atomicObj.stamp, atomicObj.stamp + 1)
}
val t2 = Thread {
val stamp: Int = atomicObj.stamp
try {
TimeUnit.SECONDS.sleep(2)
} catch (e: InterruptedException) {
}
println("run thread2")
val result: Boolean = atomicObj.compareAndSet("A", "B", stamp, stamp + 1)
println(result) // false
}
t1.start()
t2.start()
複製代碼
能夠看到因爲 Thread2
提早獲取了版本號,及時 Thread1
執行以後值依然是 A
, 但因爲版本號已然發生了變化因此 Thread2
的執行結果仍然是 false
。