版權聲明:本文的內容大都來自於「vioao」的博文,略作修改。
CAS
什麼是 CAS?
CAS(Compare And Swap
),即比較並交換,是解決多線程並行情況下使用鎖造成性能損耗的一種機制,CAS 操作包含三個操作數——內存位置V
、預期原值A
和新值B
。如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值;否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。
CAS 有效地說明了“我認爲位置V
應該包含值A
,如果包含該值,則將B
放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”在 Java 中,sun.misc.Unsafe
類提供了硬件級別的原子操作來實現這個 CAS,java.util.concurrent
包下的大量類都使用了這個Unsafe
類的 CAS 操作。
CAS 的應用
java.util.concurrent.atomic
包下的類大多是使用 CAS 操作來實現的,如AtomicInteger
、AtomicBoolean
和AtomicLong
等。下面以AtomicInteger
的部分實現來大致講解下這些原子類的實現。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;// 初始int大小
// 省略了部分代碼...
// 帶參數構造函數,可設置初始int大小
public AtomicInteger(int initialValue) {
value = initialValue;
}
// 不帶參數構造函數,初始int大小爲0
public AtomicInteger() {
}
// 獲取當前值
public final int get() {
return value;
}
// 設置值爲 newValue
public final void set(int newValue) {
value = newValue;
}
//返回舊值,並設置新值爲 newValue
public final int getAndSet(int newValue) {
/**
* 這裏使用for循環不斷通過CAS操作來設置新值
* CAS實現和加鎖實現的關係有點類似樂觀鎖和悲觀鎖的關係
* */
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
// 原子的設置新值爲update, expect爲期望的當前的值
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 獲取當前值current,並設置新值爲current+1
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
// 此處省略部分代碼,餘下的代碼大致實現原理都是類似的
}
一般來說,在競爭不是特別激烈的時候,使用該包下的原子操作性能比使用synchronized
關鍵字的方式高效的多。通過查看getAndSet()
方法,可知如果資源競爭十分激烈的話,這個for
循環可能換持續很久都不能成功跳出。在這種情況下,我們可能需要考慮如何降低對資源的競爭。在較多的場景下,我們可能會使用到這些原子類操作。一個典型應用就是計數,在多線程的情況下需要考慮線程安全問題,示例代碼如下:
public class Counter {
private int count;
public Counter(){}
public int getCount(){
return count;
}
public void increase(){
count++;
}
}
上面這個類在多線程環境下會有線程安全問題,要解決這個問題最簡單的方式可能就是加鎖,優化代碼如下:
public class Counter {
private int count;
public Counter(){}
public synchronized int getCount(){
return count;
}
public synchronized void increase(){
count++;
}
}
這是悲觀鎖的實現,如果我們需要獲取這個資源,那麼我們就給它加鎖,其他線程都無法訪問該資源,直到我們操作完後釋放對該資源的鎖。我們知道,悲觀鎖的效率是不如樂觀鎖的,上面說了atomic
包下的原子類的實現是樂觀鎖方式,因此其效率會比使用synchronized
關鍵字更高一些,推薦使用這種方式,代碼如下:
public class Counter {
private AtomicInteger count = new AtomicInteger();
public Counter(){}
public int getCount(){
return count.get();
}
public void increase(){
count.getAndIncrement();
}
}
CAS 的缺點
CAS 雖然能夠很高效的實現原子操作,但是 CAS 仍然存在三大問題。
- ABA 問題
因爲 CAS 需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A
,變成了B
,又變成了A
,那麼使用 CAS 進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA 問題的解決思路就是使用版本號,在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A
就會變成1A-2B-3A
。
從 Java 1.5 開始 JDK 的atomic
包裏提供了一個類AtomicStampedReference
來解決 ABA 問題。這個類的compareAndSet
方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
- 循環時間長開銷大
CAS 自旋如果長時間不成功,會給 CPU 帶來非常大的執行開銷。如果 JVM 能支持處理器提供的pause
指令那麼效率會有一定的提升,pause
指令有兩個作用,一是它可以延遲流水線執行指令,使 CPU 不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零;二是它可以避免在退出循環的時候因內存順序衝突而引起 CPU 流水線被清空,從而提高 CPU 的執行效率。
- 只能保證一個共享變量的原子操作
當對一個共享變量執行操作時,我們可以使用循環 CAS 的方式來保證原子操作,但是對多個共享變量操作時,循環 CAS 就無法保證操作的原子性,這個時候就需要用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a
,合併一下ij=2a
,然後用 CAS 來操作ij
。從 Java 1.5 開始 JDK 提供了AtomicReference
類來保證引用對象之間的原子性,我們可以把多個變量放在一個對象裏來進行 CAS 操作。
AQS
什麼是 AQS?
AQS(AbstractQueuedSynchronizer
),即抽象隊列同步器,是 JDK 下提供的一套用於實現基於 FIFO 等待隊列的阻塞鎖和相關的同步器的一個同步框架。這個抽象類被設計爲作爲一些可用原子int
值來表示狀態的同步器的基類。如果我們看過類似CountDownLatch
類的源碼實現,會發現其內部有一個繼承了AbstractQueuedSynchronizer
的內部類Sync
。可見CountDownLatch
是基於 AQS 框架來實現的一個同步器,類似的同步器在 JUC 下還有不少,如Semaphore
等。
AQS 的應用
如上所述,AQS 管理一個關於狀態信息的單一整數,該整數可以表現任何狀態。比如,Semaphore
用它來表現剩餘的許可數,ReentrantLock
用它來表現擁有它的線程已經請求了多少次鎖;FutureTask
用它來表現任務的狀態等。
/* To use this class as the basis of a synchronizer, redefine the
* following methods, as applicable, by inspecting and/or modifying
* the synchronization state using {@link #getState}, {@link
* #setState} and/or {@link #compareAndSetState}:
*
* <ul>
* <li> {@link #tryAcquire}
* <li> {@link #tryRelease}
* <li> {@link #tryAcquireShared}
* <li> {@link #tryReleaseShared}
* <li> {@link #isHeldExclusively}
* </ul>
* /
如 JDK 的文檔中所說,使用 AQS 來實現一個同步器需要覆蓋實現如下幾個方法,並且使用getState
、setState
和compareAndSetState
這三個方法來操作狀態。
boolean tryAcquire(int arg)
boolean tryRelease(int arg)
int tryAcquireShared(int arg)
boolean tryReleaseShared(int arg)
boolean isHeldExclusively()
以上方法不需要全部實現,根據獲取的鎖的種類可以選擇實現不同的方法,支持獨佔(排他)獲取鎖的同步器應該實現tryAcquire
、 tryRelease
、isHeldExclusively
;而支持共享獲取的同步器應該實現tryAcquireShared
、tryReleaseShared
、isHeldExclusively
。下面以CountDownLatch
舉例說明基於 AQS 實現同步器,CountDownLatch
用同步狀態持有當前計數,countDown
方法調用 release
從而導致計數器遞減;當計數器爲 0 時,解除所有線程的等待;await
調用acquire
,如果計數器爲 0,acquire
會立即返回,否則阻塞。通常用於某任務需要等待其他任務都完成後才能繼續執行的情景。源碼如下:
public class CountDownLatch {
/**
* 基於AQS的內部Sync
* 使用AQS的state來表示計數count.
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
// 使用AQS的getState()方法設置狀態
setState(count);
}
int getCount() {
// 使用AQS的getState()方法獲取狀態
return getState();
}
// 覆蓋在共享模式下嘗試獲取鎖
protected int tryAcquireShared(int acquires) {
// 這裏用狀態state是否爲0來表示是否成功,爲0的時候可以獲取到返回1,否則不可以返回-1
return (getState() == 0) ? 1 : -1;
}
// 覆蓋在共享模式下嘗試釋放鎖
protected boolean tryReleaseShared(int releases) {
// 在for循環中Decrement count直至成功;
// 當狀態值即count爲0的時候,返回false表示 signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
// 使用給定計數值構造CountDownLatch
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 讓當前線程阻塞直到計數count變爲0,或者線程被中斷
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 阻塞當前線程,除非count變爲0或者等待了timeout的時間。當count變爲0時,返回true
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// count遞減
public void countDown() {
sync.releaseShared(1);
}
// 獲取當前count值
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
AQS 實現原理淺析
AQS 的實現主要在於維護一個volatile int state
(代表共享資源)和一個 FIFO 線程等待隊列(多線程爭用資源被阻塞時會進入此隊列,此隊列稱之爲CLH
隊列)。CLH 隊列中的每個節點是對線程的一個封裝,包含線程基本信息,狀態,等待的資源類型等。
CLH結構如下:
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
下面簡單看下獲取資源的代碼:
public final void acquire(int arg) {
// 首先嚐試獲取,不成功的話則將其加入到等待隊列,再for循環獲取
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 從clh中選一個線程獲取佔用資源
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 當節點的先驅是head的時候,就可以嘗試獲取佔用資源了tryAcquire
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 如果獲取到資源,則將當前節點設置爲頭節點head
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果獲取失敗的話,判斷是否可以休息,可以的話就進入waiting狀態,直到被unpark()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private Node addWaiter(Node mode) {
// 封裝當前線程和模式爲新的節點,並將其加入到隊列中
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// tail爲null,說明還沒初始化,此時需進行初始化工作
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 否則的話,將當前線程節點作爲tail節點加入到CLH中去
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
參考資料: