一 問題引入
當我們測試多個線程操作a++的時候,會出現以下結果
public class CasDemo2 {
public static void main(String[] args) {
Castest castest=new Castest();
for(int i=0;i<10;i++){
Thread thread=new Thread(castest);
thread.start();
}
}
}
class Castest implements Runnable{
private int a=0;
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+":"+a++);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
結果並沒有按照我們想要的輸出,我們將a++這個操作其實可以細分成三個步驟:
(1)從內存中讀取a
(2)對a進行加1操作
(3)將a的值重新寫入內存中
發現6出現了三次,說明線程3,9,4獲取a的時候,並不是最新的a。此時你肯定會想到volitile關鍵字,private volatile int a=0;
但是結果還是會出現這種情況,可以自行嘗試下。我們都知道 volitile具有內存可見性,即線程A對volatile變量的修改,其他線程獲取的volatile變量都是最新的。但是volatile只提供了保證訪問該變量時,每次都是從內存中讀取最新值,並不會使用寄存器緩存該值——每次都會從內存中讀取。而對該變量的修改,volatile並不提供原子性的保證。後續會對volitile進行詳細的說明。
此時當然可以通過synchronized來保證線程安全性
class Castest implements Runnable{
private volatile int a=0;
@Override
public void run() {
try {
synchronized (this){
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+":"+a++);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private 輸出結果:
synchronized是一種獨佔鎖,也叫悲觀鎖會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。樂觀鎖用到的機制就是CAS,Compare and Swap。
二 jdk的cas實現
我們可以通過jdk源碼來分析cas的原理, 首先針對上面的線程安全性,是如何通過cas實現的呢,在jdk1.5之後有個Atomic包,可以通過該包下面的方法實現
public class CasDemo2 {
public static void main(String[] args) {
Castest castest=new Castest();
for(int i=0;i<10;i++){
Thread thread=new Thread(castest);
thread.start();
}
}
}
class Castest implements Runnable{
AtomicInteger count = new AtomicInteger();
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+":"+count.getAndIncrement());
} catch (Exception e) {
e.printStackTrace();
}
}
}
測試輸出結果:
我們可以看到結果,在多線程併發情況下,並沒有出現重複值的情況,每個線程拿到的都是不重複的值。看到這裏有些人可能會有疑惑,atomic輸出並沒有像synchronized那樣按順序輸出,爲什麼說是保證線程安全性。你可能對線程安全性有個誤解,
所謂的線程安全性說簡單點就是保證數據的正確性,跟順序關係不大,要想保證線程按順序執行方法很多,比如 線程的wait方法,join方法,wait方法,加鎖等等。舉個生活中的例子,比如a,b,c三人去購物,某個商品的庫存只有10件,不論a,b,c誰先買,庫存的邏輯正確性不會變,a買了2個,那麼b,c只能有8件可以買。跟誰先買後買沒關係,但是一定要保證這個操作的正確性。
三 分析cas源碼
點擊AtomicInteger的getAndIncrement方法,可以看到如下
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//獲得給定對象的指定偏移量offset的int值,使用volatile語義,總能獲取到最新的int值。
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
可以看到主要兩個方法,getIntVolatile與compareAndSwapInt方法,這兩個方法很好的實現了線程安全性
保證原子性的策略:
1:變量都是用Volatile關鍵字修飾。來保證內存可見性(getIntVolatile)
2:使用CAS算法,來保證原子性。(compareAndSwapInt)
關於volatile更好的說明可以查看這邊博客:java內存模型以及volatile
Cas算法源碼:
public final native boolean compareAndSwapInt(
Object var1,//操作的對象a
long var2,//對象a的地址偏移量
int var4,//對象a的期望值
int var5 //對象a的實際值
);
這個方法是native,調用C++層JVM的源碼。這裏有JVM的實現源碼下載
鏈接:https://pan.baidu.com/s/1wRVNciNbT7ABGTPbR8Qlqw
提取碼:lekq
Unsafe:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Unsafe.cpp:
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);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
核心方法就是cmpxchg(含義:compare and exchange)
由於這個有多個系統的實現,這裏只看linux_x86架構
atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
這裏使用了底層彙編語言,LOCK_IF_MP命令:根據當前系統是否爲多核處理器決定是否爲cmpxchg指令添加lock前綴。
lock的功能:
① 保證指令的執行的原子性
帶有lock前綴的指令在執行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上做了一個很有意義的優化:如果要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨佔或以修改狀態),並且該內存區域被完全包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。由於在指令執行期間該緩存行會一直被鎖定,其它處理器無法讀/寫該指令要訪問的內存區域,因此能保證指令執行的原子性。
② 禁止該指令與之前和之後的讀和寫指令重排序
在AI-32架構軟件開發者手冊第8中內存排序中,有說明LOCK前綴會禁止指令與之前和之後的讀和寫指令重排序。這相當於JMM中定義的StoreLoad內存屏障的效果。也正是因爲這個內存屏障的效果,會使得線程把其寫緩衝區中的所有數據刷新到內存中。注意,這裏不是單單被修改的數據會被回寫到主內存,而是寫緩存中所有的數據都回寫到主內存。
而將寫緩衝區的數據回寫到內存時,就會通過緩存一致性協議(如,MESI協議)和窺探技術來保證寫入的數據被其他處理器的緩存可見。
而這就相當於實現了volatile的內存語義。是的,上面我們爲說明的lock前綴是如何實現volatile的內存語義就是這麼保證的。
cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory
cmpxchgl的詳細執行過程:
首先,輸入是"r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp),表示compare_value存入eax寄存器,而exchange_value、dest、mp的值存入任意的通用寄存器。嵌入式彙編規定把輸出和輸入寄存器按統一順序編號,順序是從輸出寄存器序列從左到右從上到下以“%0”開始,分別記爲%0、%1···%9。也就是說,輸出的eax是%0,輸入的exchange_value、compare_value、dest、mp分別是%1、%2、%3、%4。
因此,cmpxchgl %1,(%3)實際上表示cmpxchgl exchange_value,(dest),此處(dest)表示dest地址所存的值。需要注意的是cmpxchgl有個隱含操作數eax,其實際過程是先比較eax的值(也就是compare_value)和dest地址所存的值是否相等,如果相等則把exchange_value的值寫入dest指向的地址。如果不相等則把dest地址所存的值存入eax中。
輸出是"=a" (exchange_value),表示把eax中存的值寫入exchange_value變量中。
Atomic::cmpxchg這個函數最終返回值是exchange_value,也就是說,如果cmpxchgl執行時compare_value和dest指針指向內存值相等則會使得dest指針指向內存值變成exchange_value,最終eax存的compare_value賦值給了exchange_value變量,即函數最終返回的值是原先的compare_value。此時Unsafe_CompareAndSwapInt的返回值(jint)(Atomic::cmpxchg(x, addr, e)) == e就是true,表明CAS成功。如果cmpxchgl執行時compare_value和(dest)不等則會把當前dest指針指向內存的值寫入eax,最終輸出時賦值給exchange_value變量作爲返回值,導致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到false,表明CAS失敗。
四 cas原理
CAS具體執行時,當且僅當預期值A符合內存地址V中存儲的值時,就用新值U替換掉舊值,並寫入到內存地址V中。否則不做更新。
CAS會有如下三個方面的問題:
1.ABA問題,一個線程將內存值從A改爲B,另一個線程又從B改回到A。
2.循環時間長開銷大:CAS算法需要不斷地自旋來讀取最新的內存值,長時間讀取不到就會造成不必要的CPU開銷。
3. 只能保證一個共享變量的原子操作(jdk的AtomicReference來保證應用對象之間的原子性,可以把多個變量放在一個對象裏來進行CAS操作,解決了這一問題)。
ABA問題解決方案:在變量前面添加版本號,每次變量更新的時候都將版本號加1,比如juc的原子包中的AtomicStampedReference類。
參考資料:https://www.cnblogs.com/wildwolf0/p/11455796.html
https://www.jianshu.com/p/bd68ddf91240