1. 程式人生 > >《Java併發程式設計的藝術》筆記四——Java如何實現原子操作.md

《Java併發程式設計的藝術》筆記四——Java如何實現原子操作.md

在Java中可以通過鎖和迴圈CAS的方式實現原子操作。

注:CAS(比較與交換,Compare and swap) 是一種有名的無鎖演算法。CAS的語義是“我認為V的值應該為A,如果是,那麼將V的值更新為B,否則不修改並告訴V的值實際為多少”,CAS是一種 樂觀鎖 技術,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。

CAS是以原子操作為基礎,採用事務->提交->提交失敗->重試這樣特定程式設計手法的機制,它使得正在訪問共享資源的執行緒不依賴於任何其它執行緒的排程和執行,並且能夠在有限的步驟內完成。

1.使用迴圈CAS實現原子操作。

JVM中的CAS操作是利用了處理器提供的CMPXCHG指令實現的。自旋CAS的基本思路就是迴圈進行CAS操作,直到成功為止。

以下程式碼實現了一個基於CAS的執行緒安全的計數器和一個非執行緒安全的計數器:


package wha.thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
* 使用鎖和迴圈CAS實現原子操作
*/
public class Counter {
   private
int i = 0; private AtomicInteger atomicI = new AtomicInteger(0); public void count(){ i++; } public void safeCount(){ while(true){ int j = atomicI.get(); boolean suc = atomicI.compareAndSet(j, ++j); if(suc) break; } } public static
void main(String[] args) { final Counter cas = new Counter(); List<Thread> ts = new ArrayList<>(600); long start = System.currentTimeMillis(); for (int k=0; k<100; k++){ Thread t = new Thread(new Runnable() { @Override public void run() { cas.count(); cas.safeCount(); } }); ts.add(t); // Thread t = new Thread(()->{//lambda // for (int m=0; m<10000;m++){ // cas.count(); // cas.safeCount(); // } // }); // ts.add(t); } ts.forEach(thread -> thread.start()); for (Thread t: ts){ try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(cas.i); System.out.println(cas.atomicI.get()); System.out.println("time:"+(System.currentTimeMillis()-start)); } } } 執行的結果: ... 978173 982162 time:177 ... 996010 1000000 time:184

從JDK1.5版本開始,JDK的併發包中提供了以Atomic開頭的類,來支援原子操作,比如AtomicBoolean,AtomicInteger,AtomicLong等。

2.CAS實現原子操作的3大問題

CAS雖然很高效的解決了原子操作,但是依然存在幾個問題

1.ABA問題

因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新。但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查的時候會發現他的值沒有變化,但實際上變化了。ABA問題的解決思路是,使用版本號。在變數前面追加版本號,每次變數更新的時候把版本號+1,這樣A->B->A,就會變成1A->2B->3A。

2.迴圈時間長開銷大

自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。

3.只能保證一個共享變數的原子操作

當CAS對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候可以用鎖。還有一個取巧的方法,就是把多喝共享變數合併成一個共享變數來操作。例如,兩個共享變數i=1,j=a,合併一下ij=1a,然後用CAS來操作ij。

從Java1.5開始,JDK提供了AtomicReference類來保證應用物件之間的原子性,就可以把多個變數放在一個物件裡來進行CAS操作。

3.使用鎖機制來實現原子操作

鎖機制保證了執行緒只有在獲得鎖之後才能操作鎖定的記憶體區域。JVM內部實現了很多種鎖機制,有偏向鎖,輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現鎖的方式都用了迴圈CAS,即當一個執行緒想進入同步塊的時候使用迴圈CAS的方式來獲得鎖,當它退出同步塊的時候使用迴圈CAS釋放鎖。