研究java併發編程有一段時間了, 在併發編程中cas出現的次數極爲頻繁。cas的英文全名叫作compare and swap,意思很簡單就是比較並交換。在jdk的conurrent包中,Doug Lea大神大量使用此技術,實現了多線程的安全性。
cas的核心思想就是獲取當前的內存偏移值、指望值和更新值,若是根據內存偏移值獲得的變量等於指望值,則進行更新。java
總有面試官喜歡問你i++和++i,以及經典的字符串問題,其實這些問題只要你試用javap -c這個命令反編譯一下,就一目瞭然。固然今天的主題是cas,我首先來研究下a++:c++
//@RunWith(SpringRunner.class) //@SpringBootTest public class SblearnApplicationTests { public static volatile int a; public static void main(String[] args) { a++; } }
經過javac SblearnApplicationTests.java,javap -c SblearnApplicationTests.class能夠獲得:面試
Compiled from "SblearnApplicationTests.java" public class com.example.sblearn.SblearnApplicationTests { public static volatile int a; public com.example.sblearn.SblearnApplicationTests(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: getstatic #2 // Field a:I 3: iconst_1 //當int取值-1~5採用iconst指令,取值-128~127採用bipush指令,取值-32768~32767採用sipush指令,取值-2147483648~2147483647採用 ldc 指令 4: iadd 5: putstatic #2 // Field a:I 8: return }
經過反編譯得出如上的結果,都是一些jvm的指令,百度一下就能知道意思。咱們將變量a用violate修飾,保證線程間的可見性。經過jvm指令可知a++不是一個原子動做,若是多個線程同事對a進行操做,沒法保證線程安全,那怎麼解決呢?編程
java給咱們提供了一個關鍵字synchronized,能夠對成員方法、靜態方法、代碼塊進行加鎖,從而保證操做的原子性。但效率不高,還有其餘辦法嗎?固然有了,就是咱們今天的主角cas。接下來咱們再來看看concurrent包下的AtomicInteger:windows
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 static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; //效果等同於a++,但保證了原子性 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } }
public final class Unsafe { public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public native int getIntVolatile(Object var1, long var2); //object var1:當前AtomicInteger對象,long var2Integer對象的內存偏移值,int var4 增長的值 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; } }
cas機制的核心類就是Unsafe,valueOffset 是其內存偏移值。因爲java語言沒法直接操做底層,須要本地方法(native method)來訪問,unsafe這個類中存在大量本地方法,就是在調用c去操做特定內存的數據。咱們先假設unsafe幫咱們保證了原子性,先來分析下AtomicInteger.getAndIncrement(),在jdk1.8中,其實現就是Unsafe.getAndAddInt()緩存
咱們經過cas保證了對value的併發線程安全,其安全的保證是CAS經過調用JNI的代碼實現的。JNI:Java Native Interface爲JAVA本地調用,容許java調用其餘語言。而compareAndSwapInt就是藉助C來調用CPU底層指令實現的。下面從分析比較經常使用的CPU(intel x86)來解釋CAS的實現原理。compareAndSwapInt方法在openjdk中依次調用的c++代碼爲:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。這個本地方法的最終實如今openjdk的以下位置:openjdk-7-fcs-src-b147-27jun2011openjdkhotspotsrcoscpuwindowsx86vm atomicwindowsx86.inline.hpp(對應於windows操做系統,X86處理器)。下面是對應於intel x86處理器的源代碼的片斷:安全
// Adding a lock prefix to an instruction on MP machine // VC++ doesn't like the lock prefix to be on a single line // so we can't insert a label after the lock prefix. // By emitting a lock prefix, we can define a label after it. #define LOCK_IF_MP(mp) __asm cmp mp, 0 \ __asm je L0 \ __asm _emit 0xF0 \ __asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { // alternative for InterlockedCompareExchange 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 } }
如上面源代碼所示,程序會根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴。若是程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(lock cmpxchg)。反之,若是程序是在單處理器上運行,就省略lock前綴(單處理器自身會維護單處理器內的順序一致性,不須要lock前綴提供的內存屏障效果)。多線程
intel的手冊對lock前綴的說明以下:併發
1.確保對內存的讀-改-寫操做原子執行。在Pentium及Pentium以前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其餘處理器暫時沒法經過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上作了一個頗有意義的優化:若是要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨佔或以修改狀態),而且該內存區域被徹底包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。因爲在指令執行期間該緩存行會一直被鎖定,其它處理器沒法讀/寫該指令要訪問的內存區域,所以能保證指令執行的原子性。這個操做過程叫作緩存鎖定(cache locking),緩存鎖定將大大下降lock前綴指令的執行開銷,可是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。
2.禁止該指令與以前和以後的讀和寫指令重排序。
3.把寫緩衝區中的全部數據刷新到內存中。jvm
cas的缺點就是會出現aba問題,假如一個字母爲a,它經歷a->b->a的過程,實際已經改變兩次,但值相同。部分業務場景是不容許出現這種狀況的(好比銀行轉帳..).解決辦法就是添加版本號,他就變成了1a->2b>3a。jdk1.5以後也提供了AtomicStampedReference來解決aba問題。
自旋cas若是長時間不成功,將會對cpu帶來很是大的開銷。cas只能保證一個共享變量的原子操做。因此很是簡單的操做又不想引入鎖,cas是一個很是好的選擇。