1 什么是原子操作?如何實現(xiàn)原子操作推励?
2 CAS的原理
在計算機科學(xué)中蜻懦,比較和交換(Conmpare And Swap)是用于實現(xiàn)多線程同步的原子指令夸溶。 它將內(nèi)存位置的內(nèi)容與給定值進行比較,只有在相同的情況下损肛,將該內(nèi)存位置的內(nèi)容修改為新的給定值厢破。 這是作為單個原子操作完成的。 原子性保證新值基于最新信息計算; 如果該值在同一時間被另一個線程更新治拿,則寫入將失敗摩泪。 操作結(jié)果必須說明是否進行替換; 這可以通過一個簡單的布爾響應(yīng)(這個變體通常稱為比較和設(shè)置),或通過返回從內(nèi)存位置讀取的值來完成(摘自維基本科)
- CAS(Compare And Swap)劫谅,指令級別保證這是一個原子操作
- 三個運算符: 一個內(nèi)存地址V见坑,一個期望的值A(chǔ),一個新值B
- 基本思路:如果地址V上的值和期望的值A(chǔ)相等捏检,就給地址V賦給新值B荞驴,如果不是,不做任何操作贯城。
- 循環(huán)(死循環(huán)熊楼,自旋)里不斷的進行CAS操作
3 JDK提供的CAS操作類
- 更新基本類型類:AtomicBoolean,AtomicInteger能犯,AtomicLong鲫骗,AtomicReference
- 更新數(shù)組類:AtomicIntegerArray,AtomicLongArray悲雳,AtomicReferenceArray
- 更新引用類型:AtomicReference挎峦,AtomicMarkableReference,AtomicStampedReference
- 原子更新字段類: AtomicReferenceFieldUpdater合瓢,AtomicIntegerFieldUpdater坦胶,AtomicLongFieldUpdater
4 CAS在JUC中的運用
JUC中非常重要的一個類AbstractQueuedSynchronizer,作為JAVA中多種鎖實現(xiàn)的父類晴楔,其中有很多地方使用到了CAS操作以提升并發(fā)的效率.
此段源碼是往queue隊列中添加node節(jié)點顿苇,利用了自旋和CAS原理
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上面為同步隊列的入隊操作,也是一種樂觀鎖的實現(xiàn)税弃,多線程情況下纪岁,操作頭節(jié)點和尾節(jié)點都有可能失敗,失敗后會再次嘗試则果,直到成功幔翰。
5 CAS的問題
5.1 ABA問題
什么是ABA問題
如線程1從內(nèi)存X中取出A漩氨,這時候另一個線程2也從內(nèi)存X中取出A,并且線程2進行了一些操作將內(nèi)存X中的值變成了B遗增,然后線程2又將內(nèi)存X中的數(shù)據(jù)變成A叫惊,這時候線程1進行CAS操作發(fā)現(xiàn)內(nèi)存X中仍然是A,然后線程1操作成功做修。雖然線程1的CAS操作成功霍狰,但是整個過程就是有問題的。比如鏈表的頭在變化了兩次后恢復(fù)了原值饰及,但是不代表鏈表就沒有變化蔗坯。
解決方法
- 使用版本號
ABA問題的解決思路是使用版本號,每次變量更新的時候版本號加1燎含,那么A->B->A就會變成1A->2B->3A - jdk自帶原子變量
從jdk1.5開始宾濒,jdk的Atomic包里就提供了一個類AtomicStampedReference來解決ABA問題,這個類中的compareAndSet方法的作用就是首先檢查當(dāng)前引用是否等于預(yù)期引用瘫镇,并且檢查當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志鼎兽,如果全部相等,則以原子方式將該引用和該標(biāo)志的值更新為指定的新值
/**
* 如果當(dāng)前引用等于預(yù)期引用并且當(dāng)前標(biāo)志等于預(yù)期標(biāo)志
* 則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定新值
*
* @param expectedReference 預(yù)期引用值
* @param newReference 新的引用值
* @param expectedStamp 預(yù)期標(biāo)記值
* @param newStamp 新標(biāo)記值
* @return {@code true} if successful
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
#預(yù)期引用==當(dāng)前引用
expectedReference == current.reference &&
#預(yù)期標(biāo)志==當(dāng)前標(biāo)志
expectedStamp == current.stamp &&
#新引用==當(dāng)前引用 并且 新標(biāo)志==當(dāng)前標(biāo)志
((newReference == current.reference &&
newStamp == current.stamp) ||
#原子更新值
casPair(current, Pair.of(newReference, newStamp)));
}
5.2 開銷問題
循環(huán)時間長開銷大
自旋CAS如果長時間不成功铣除,會給CPU帶來非常大的執(zhí)行開銷谚咬。如果jvm能支持處理器提供的pause指令,那么效率會有一定的提升尚粘。pause指令有兩個作用:
第一择卦,它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會消耗過多的執(zhí)行資源郎嫁,延遲的時間取決于具體實現(xiàn)的版本秉继,在一些處理器上延遲時間是零。
第二泽铛,它可以避免在退出循環(huán)的時候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush)尚辑,從而提高CPU的執(zhí)行效率。
5.3 只能保證一個共享變量的原子操作
CAS機制所保證的只是一個變量的原子性操作盔腔,而不能保證整個代碼塊的原子性杠茬。比如需要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了弛随。