文章目錄
1 什麼是CAS?
1.1 加鎖和CAS解決原子性問題的不同原理
首先看如下代碼:
package com.nrsc.ch2.cas;
import java.util.ArrayList;
import java.util.List;
public class CasDemo {
//共享資源
static int i = 0;
public static void increase() {
i++;
}
public static void main(String[] args) throws InterruptedException {
Runnable r = () -> {
for (int j = 0; j < 1000; j++) {
increase();
}
};
List<Thread> threads = new ArrayList<>();
for (int j = 0; j < 10; j++) {
Thread thread = new Thread(r);
threads.add(thread);
thread.start();
}
//確保前面10個線程都走完
for (Thread thread : threads) {
thread.join();
}
System.out.println(i);
}
}
相信每個人都知道這段代碼由於i++不是原子操作
,因此會導致這10個線程執行後的最終結果不是10*1,000 = 10,000。
當然也相信幾乎所有人都知道通過加鎖可以解決這個問題
,加鎖方式解決該問題的原理基本可以用下圖進行概況:
而其實除了加鎖之外利用CAS機制也能解決這個問題。既然說它是除了加鎖之外的另一種解決方式,那它肯定是無鎖的
,因此利用CAS機制解決該問題的方式大致可以用下圖進行概況:
那到底啥是CAS呢?它又是是如何解決這個問題的呢?
1.2 CAS原理分析
先看定義(摘自百度百科):
compare and swap,解決多線程並行情況下使用鎖造成性能損耗的一種機制,CAS操作包含三個操作數——內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。否則,處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了
“我認爲位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。"
再畫個圖解釋一下:
2 CAS可能的問題
2.1 ABA問題
說實話剛接觸CAS的時候,其實我就想到ABA這個問題了。☺☺☺
通過1.2可以知道,CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化就更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了 —> 這就是所謂的ABA問題。
舉個通俗點的例子,你倒了一杯水放桌子上,幹了點別的事,然後同事把你水喝了又給你重新倒了一杯水,你回來看水還在,拿起來就喝,如果你不管水中間被人喝過,只關心水還在,還好 ; 但是假若你是一個比較講衛生的人,那你肯定就不高興了。。。
ABA問題的解決思路其實也很簡單,就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A了。
2.2 循環時間長開銷大
自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。
2.3只能保證一個共享變量的原子操作
當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,
這個時候就可以用鎖
。
還有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如,有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象裏來進行CAS操作。
3 JDK中對CAS的支持 — Unsafe類
java中提供了對CAS操作的支持,具體在sun.misc.unsafe類中,聲明如下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
上面各個參數的含義:
- 參數var1:表示要操作的對象
- 參數var2:表示要操作對象中屬性地址的偏移量
- 參數var4:表示需要修改數據的期望的值
- 參數var5/var6:表示雷要修改爲的新值
注意:
Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。過度的使 用Unsafe類會使得出錯的機率變大,因此Java官方並不建議使用的,官方文檔也幾乎沒有。Unsafe對 象不能直接調用,只能通過反射獲得。
4 JDK中的相關原子操作類簡介 — 底層CAS機制
4.1 AtomicInteger
- int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果。
- boolean compareAndSet(int expect,int update):如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值。
- int getAndIncrement():以原子方式將當前值加1,注意,這裏返回的是自增前的值。
- int getAndSet(int newValue):以原子方式設置爲newValue的值,並返回舊值。
4.2 AtomicIntegerArray
主要是提供原子的方式更新數組裏的整型,其常用方法如下。
- int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加。
- boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。
需要注意的是,數組value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前數組複製一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組。
4.3更新引用類型
原子更新基本類型的AtomicInteger,只能更新一個變量,如果要原子更新多個變量,就需要使用這個原子更新引用類型提供的類。Atomic包提供了以下3個類。
- AtomicReference
原子更新引用類型。 - AtomicStampedReference
利用版本戳的形式記錄了每次改變以後的版本號,這樣的話就不會存在ABA問題了。這就是AtomicStampedReference的解決方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作爲計數器使用,AtomicMarkableReference的pair使用的是boolean mark。 還是那個水的例子,AtomicStampedReference可能關心的是動過幾次,AtomicMarkableReference關心的是有沒有被人動過,方法都比較簡單。
- AtomicMarkableReference:
原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。
4.4 原子更新字段類
如果需原子地更新某個類裏的某個字段時,就需要使用原子更新字段類,Atomic包提供了以下3個類進行原子字段更新。
要想原子地更新字段類需要兩步。第一步,因爲原子更新字段類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()創建一個更新器,並且需要設置想要更新的類和屬性。 第二步,更新類的字段(屬性)必須使用public volatile修飾符。
- AtomicIntegerFieldUpdater:
原子更新整型的字段的更新器。 - AtomicLongFieldUpdater:
原子更新長整型字段的更新器。 - AtomicReferenceFieldUpdater:
原子更新引用類型裏的字段。
5 樂觀鎖和悲觀鎖
悲觀鎖
從悲觀的角度出發: (總有刁民想害朕)
總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這
樣別人想拿這個數據就會阻塞。因此synchronized我們也將其稱之爲悲觀鎖。JDK中的ReentrantLock
也是一種悲觀鎖。性能較差!
樂觀鎖
從樂觀的角度出發:
總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,就算改了也沒關係,再重試即可。所
以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去修改這個數據,如何沒有人修改則更
新,如果有人修改則重試。
CAS這種機制也可以將其稱之爲樂觀鎖。綜合性能較好!
CAS獲取共享變量時,爲了保證該變量的可見性,需要使用volatile修飾。結合CAS和volatile可以實現無鎖併發,適用於競爭不激烈、多核 CPU 的場景下。
- (1)因爲沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一。
- (2)但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響。