學習Java併發編程,CAS機制都是一個不得不掌握的知識點。這篇文章主要是從出現的緣由再到原理進行一個解析。但願對你有所幫助。編程
爲何須要CAS機制呢?咱們先從一個錯誤現象談起。咱們常用volatile關鍵字修飾某一個變量,代表這個變量是全局共享的一個變量,同時具備了可見性和有序性。可是卻沒有原子性。好比說一個常見的操做a++。這個操做其實能夠細分紅三個步驟:安全
(1)從內存中讀取a多線程
(2)對a進行加1操做併發
(3)將a的值從新寫入內存中app
在單線程狀態下這個操做沒有一點問題,可是在多線程中就會出現各類各樣的問題了。由於可能一個線程對a進行了加1操做,還沒來得及寫入內存,其餘的線程就讀取了舊值。形成了線程的不安全現象。如何去解決這個問題呢?最多見的方式就是使用AtomicInteger來修飾a。咱們能夠看一下代碼:ide
public class Test3 {
//使用AtomicInteger定義a
static AtomicInteger a = new AtomicInteger();
public static void main(String[] args) {
Test3 test = new Test3();
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
//使用getAndIncrement函數進行自增操做
System.out.println(a.incrementAndGet());
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
如今咱們使用AtomicInteger類而且調用了incrementAndGet方法來對a進行自增操做。這個incrementAndGet是如何實現的呢?咱們能夠看一下AtomicInteger的源碼。函數
/**
* Atomically increments by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
咱們到這一步能夠看到其實就是usafe調用了getAndAddInt的方法實現的,可是如今咱們還看不出什麼,咱們再深刻到源碼中看看getAndAddInt方法又是如何實現的,oop
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;
}
到了這一步就稍微有點眉目了,原來底層調用的是compareAndSwapInt方法,這個compareAndSwapInt方法其實就是CAS機制。所以若是咱們想搞清楚AtomicInteger的原子操做是如何實現的,咱們就必需要把CAS機制搞清楚,這也是爲何咱們須要掌握CAS機制的緣由。性能
一、基本含義學習
CAS全拼又叫作compareAndSwap,從名字上的意思就知道是比較交換的意思。比較交換什麼呢?
過程是這樣:它包含 3 個參數 CAS(V,E,N),V表示要更新變量的值,E表示預期值,N表示新值。僅當 V值等於E值時,纔會將V的值設爲N,若是V值和E值不一樣,則說明已經有其餘線程作兩個更新,則當前線程則什麼都不作。最後,CAS 返回當前V的真實值。
咱們舉一個我以前舉過的例子來講明這個過程:
好比說給你兒子訂婚。你兒子就是內存位置,你本來覺得你兒子是和楊貴妃在一塊兒了,結果在訂婚的時候發現兒子身邊是西施。這時候該怎麼辦呢?你一氣之下不作任何操做。若是兒子身邊是你預想的楊貴妃,你一看很開心就給他們訂婚了,也叫做執行操做。如今你應該明白了吧。
CAS 操做時抱着樂觀的態度進行的,它老是認爲本身能夠成功完成操做。因此CAS也叫做樂觀鎖,那什麼是悲觀鎖呢?悲觀鎖就是咱們以前赫赫有名的synchronized。悲觀鎖的思想你能夠這樣理解,一個線程想要去得到這個鎖可是卻獲取不到,必需要別人釋放了才能夠。
二、底層原理
想要弄清楚其底層原理,深刻到源碼是最好的方式,在上面咱們已經經過源碼看到了其實就是Usafe的方法來完成的,在這個方法中使用了compareAndSwapInt這個CAS機制。所以,如今咱們有必要進一步深刻進去看看:
public final class Unsafe {
// compareAndSwapInt 是 native 類型的方法
public final native boolean compareAndSwapInt(
Object o,
long offset,
int expected,
int x
);
//剩餘還有不少方法
}
咱們能夠看到這裏面主要有四個參數,第一個參數就是咱們操做的對象a,第二個參數是對象a的地址偏移量,第三個參數表示咱們期待這個a是什麼值,第四個參數表示的是a的實際值。
不過這裏咱們會發現這個compareAndSwapInt是一個native方法,也就是說再往下走就是C語言代碼,若是咱們保持好奇心,能夠繼續深刻進去看看。
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe,
jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 根據偏移量valueOffset,計算 value 的地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 調用 Atomic 中的函數 cmpxchg來進行比較交換
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
上面的代碼咱們解讀一下:首先使用jint計算了value的地址,而後根據這個地址,使用了Atomic的cmpxchg方法進行比較交換。如今問題又拋給了這個cmpxchg,真實實現的是這個函數。咱們再進一步深刻看看,真相已經離咱們不遠了。
unsigned Atomic::cmpxchg(unsigned int exchange_value,
volatile unsigned int* dest,
unsigned int compare_value) {
assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
/*
* 根據操做系統類型調用不一樣平臺下的重載函數,
這個在預編譯期間編譯器會決定調用哪一個平臺下的重載函數
*/
return (unsigned int)Atomic::cmpxchg((jint)exchange_value,
(volatile jint*)dest, (jint)compare_value);
}
皮球又一次被完美的踢走了,如今在不一樣的操做系統下會調用不一樣的cmpxchg重載函數,我如今用的是win10系統,因此咱們看看這個平臺下的實現,彆着急再往下走走:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest,
jint compare_value) {
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
這塊的代碼就有點涉及到彙編指令相關的代碼了,到這一步就完全接近真相了,首先三個move指令表示的是將後面的值移動到前面的寄存器上。而後調用了LOCK_IF_MP和下面cmpxchg彙編指令進行了比較交換。如今咱們不知道這個LOCK_IF_MP和cmpxchg是如何交換的,不要緊咱們最後再深刻一下。
真相來了,他來了,他真的來了。
inline jint Atomic::cmpxchg (jint exchange_value,
volatile jint* dest, jint compare_value) {
//一、 判斷是不是多核 CPU
int mp = os::is_MP();
__asm {
//二、 將參數值放入寄存器中
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
//三、LOCK_IF_MP指令
cmp mp, 0
//四、 若是 mp = 0,代表線程運行在單核CPU環境下。此時 je 會跳轉到 L0 標記處,直接執行 cmpxchg 指令
je L0
_emit 0xF0
//五、這裏真正實現了比較交換
L0:
/*
* 比較並交換。簡單解釋一下下面這條指令,熟悉彙編的朋友能夠略過下面的解釋:
* cmpxchg: 即「比較並交換」指令
* dword: 全稱是 double word 表示兩個字,一共四個字節
* ptr: 全稱是 pointer,與前面的 dword 連起來使用,代表訪問的內存單元是一個雙字單元
* 這一條指令的意思就是:
將 eax 寄存器中的值(compare_value)與 [edx] 雙字內存單元中的值進行對比,
若是相同,則將 ecx 寄存器中的值(exchange_value)存入 [edx] 內存單元中。
*/
cmpxchg dword ptr [edx], ecx
}
}
到這一步了,相信你應該理解了這個CAS真正實現的機制了吧,最終是由操做系統的彙編指令完成的。
三、CAS機制的優缺點
(1)優勢
一開始在文中咱們曾經提到過,cas是一種樂觀鎖,並且是一種非阻塞的輕量級的樂觀鎖,什麼是非阻塞式的呢?其實就是一個線程想要得到鎖,對方會給一個迴應表示這個鎖能不能得到。在資源競爭不激烈的狀況下性能高,相比synchronized重量鎖,synchronized會進行比較複雜的加鎖,解鎖和喚醒操做。
(2)缺點
缺點也是一個很是重要的知識點,由於涉及到了一個很是著名的問題,叫作ABA問題。假設一個變量 A ,修改成 B以後又修改成 A,CAS 的機制是沒法察覺的,但實際上已經被修改過了。這就是ABA問題,
ABA問題會帶來大量的問題,好比說數據不一致的問題等等。咱們能夠舉一個例子來解釋說明。
你有一瓶水放在桌子上,別人把這瓶水喝完了,而後從新倒上去。你再去喝的時候發現水仍是跟以前同樣,就誤覺得是剛剛那杯水。若是你知道了真相,那是別人用過了你還會再用嘛?舉一個比較黃一點的例子,女友被別人睡過以後又還回來,仍是以前的那個女友嘛?
ABA能夠有不少種方式來解決,咱們在後續的文章中再進行敘述和討論。