CAS的全稱是CompareAndSwap,比較並交換,是Java保證原子性的一種重要方法,也是一種樂觀鎖的實現方式。
它需要先提前一步獲取舊值,然后進入此方法比較當下的值是否與舊值相同,如果相同,則更新數據,否則退出方法,重復一遍剛才的動作。由此可見,CAS方法是非堵塞的。CAS方法需要三個參數,變量內存值、舊的預期值、數據更新值
CAS的偽代碼可以表示為:
do{
獲取備份舊數據;
准備更新的數據;
}while( !CAS( 內存地址,備份的舊數據,新數據 ))
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset;
/** * Atomically sets the value to the given updated value * if the current value {@code ==} the expected value. * * @param expect the expected value * @param update the new value * @return {@code true} if successful. False return indicates that * the actual value was not equal to the expected value. */ public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
上面是原子類AtomicInteger的局部源碼,可以看出來Java中的CAS操作都是由Sun包下的Unsafe類實現的,而unsafe類中的方法都是Native方法,都是由JVM本地實現。
然后簡單去網上找了找相關的openJDK源碼,它調用了Atomic:comxchg方法,這個方法的實現放在了hotspot下的os_cpu包中的,也就是說它調用了操作系統和CPU層級的東西。
經過進一步了解獲知,這個方法在Linux x86系統的多處理器情況下是通過調用LOCK指令來實現的,立刻寫會內存且令其他緩存行失效。
CAS的問題:
1、ABA問題,比如剛開始讀取到備份是3,然后被其他線程連續修改兩次,最終結果還是3,那么CAS很可能識別不到數據發生了改變,這種情況對程序造成了極大的安全隱患。可以通過添加版本號等標志位來解決該問題。
2、循環時間長開銷大,如果長時間自旋不成功,會給CPU帶來很大開銷。可以使用自適應自旋鎖解決這個問題
3、只能保證一個共享變量的原子操作。比如AtomicInteger都是每次只能對一個變量進行原子性控制。
單次CAS操作的開銷:
這是一個8核CPU的計算機系統,先簡單介紹一下CPU的硬件結構體系。每個CPU都有一個高速緩存區Cache(寄存器),每兩個CPU之間有一段互聯模塊可以讓管芯內的兩個核可以互相通信,最中間還有個系統互聯模塊可以讓四個管芯進行通訊。數據以“緩存線”為單位在系統中傳輸,緩存線對應內存中2的整數冪的一個字節塊
計算機系統中,當CPU要獲取一個變量值的時候,必須要將包含了該變量的緩存線保存到自己的寄存器中;CPU要向主存寫入值的時候,根據緩存一致性協議,必須保證其他CPU獲知此操作,或者讓其他CPU刪除該緩存線的拷貝。
因此,如果CPU1和CPU5同時持有一個變量的緩存線,然后CPU5想進行CAS操作,那么它需要訪問CPU5、6的互聯模塊,未找到持有該緩存線的CPU,然后訪問系統級的互聯模塊,發現第一處互聯模塊有該緩存線,繼而訪問第一處互聯模塊,最終找到了CPU1。此時才算是完成了大部分操作。因此如果從最優角度考慮,CAS所花費的時鍾周期較鎖來說要小很多。
在Java中,sun.misc.Unsafe類提供了硬件級別的院子操作來實現這個CAS,JUC包下的大量類都使用了這個Unsafe.java類的CAS操作。