什麼是自旋鎖
說道自旋鎖就要從多線程下的鎖機制說起,由於在多處理器系統環境中有些資源因為其有限性,有時需要互斥訪問(mutual exclusion),這時會引入鎖的機制,只有獲取了鎖的進程才能獲取資源訪問。即每次只能有且只有一個進程能獲取鎖,才能進入自己的臨界區,同一時間不能兩個或兩個以上進程進入臨界區,當退出臨界區時釋放鎖。
設計互斥算法時總是會面臨一種情況,即沒有獲得鎖的進程怎麼辦?
通常有2種處理方式:
一種是沒有獲得鎖的調用者就一直循環在那裡看是否該自旋鎖的保持者已經釋放了鎖,這就是本文的重點――自旋鎖。他不用將線城阻塞起來(NON-BLOCKING)。
另一種是沒有獲得鎖的進程就阻塞(BLOCKING)自己,繼續執行線程上的其他任務,這就是――互斥鎖(包括內置鎖Synchronized還有ReentrantLock等等)。
引言
CAS(Compare and swap),即比較並交換,也是實現我們平時所說的自旋鎖或樂觀鎖的核心操作。
它的實現很簡單,就是用一個預期的值和內存值進行比較,如果兩個值相等,就用預期的值替換內存值,並返回true。否則,返回false。
保證原子操作
任何技術的出現都是為了解決某些特定的問題, CAS 要解決的問題就是保證原子操作。原子操作是什麼,原子就是最小不可拆分的,原子操作就是最小不可拆分的操作,也就是說操作一旦開始,就不能被打斷,知道操作完成。在多線程環境下,原子操作是保證線程安全的重要手段。舉個例子來說,假設有兩個線程在工作,都想對某個值做修改,就拿自增操作來說吧,要對一個整數i 進行自增操作,需要基本的三個步驟:
1、讀取i 的當前值;
2、對i 值進行加1 操作;
3、將i 值寫回內存;
假設兩個進程都讀取了i 的當前值,假設是0,這時候A 線程對i 加1 了,B 線程也加1,最後i 的是1 ,而不是2。這就是因為自增操作不是原子操作,分成的這三個步驟可以被干擾。如下面這個例子,10個線程,每個線程都執行10000 次i++ 操作,我們期望的值是100,000,但是很遺憾,結果總是小於100,000 的。
static int i = 0; public static void add(){ i++; } private static class Plus implements Runnable{ @Override public void run(){ for(int k = 0;k<10000;k++){ add(); } } } public static void main(String[] args) throws InterruptedException{ Thread[] threads = new Thread[10]; for(int i = 0;i<10;i++){ threads[i] = new Thread(new Plus()); threads[i].start(); } for(int i = 0;i<10;i++){ threads[i].join(); } System.out.println(i); }既然這樣,那怎麼辦。沒錯,也許你已經想到了,可以加鎖或者利用synchronized 實現,例如,將add() 方法修改為如下這樣:
public synchronized static void add(){ i++; }或者,加鎖操作,例如下面使用ReentrantLock (可重入鎖)實現。
private static Lock lock = new ReentrantLock(); public static void add(){ lock.lock(); i++; lock.unlock(); } CAS 實現自旋鎖
既然用鎖或synchronized 關鍵字可以實現原子操作,那麼為什麼還要用CAS 呢,因為加鎖或使用synchronized 關鍵字帶來的性能損耗較大,而用CAS 可以實現樂觀鎖,它實際上是直接利用了CPU 層面的指令,所以性能很高。
上面也說了,CAS 是實現自旋鎖的基礎,CAS 利用CPU 指令保證了操作的原子性,以達到鎖的效果,至於自旋呢,看字面意思也很明白,自己旋轉,翻譯成人話就是循環,一般是用一個無限循環實現。這樣一來,一個無限循環中,執行一個CAS 操作,當操作成功,返回true 時,循環結束;當返回false 時,接著執行循環,繼續嘗試CAS 操作,直到返回true。
其實JDK 中有好多地方用到了CAS ,尤其是java.util.concurrent包下,比如CountDownLatch、Semaphore、ReentrantLock 中,再比如java.util.concurrent.atomic 包下,相信大家都用到過Atomic* ,比如AtomicBoolean、AtomicInteger 等。
這裡拿AtomicBoolean 來舉個例子,因為它足夠簡單。
public class AtomicBoolean implements java.io.Serializable { private static final long serialVersionUID = 4654671469794556979L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicBoolean.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; public final boolean get() { return value != 0; } public final boolean compareAndSet(boolean expect, boolean update) { int e = expect ? 1 : 0; int u = update ? 1 : 0; return unsafe.compareAndSwapInt(this, valueOffset, e, u); }}這是AtomicBoolean 的部分代碼,我們看到這裡面又幾個關鍵方法和屬性。
1、使用了sun.misc.Unsafe 對象,這個類提供了一系列直接操作內存對象的方法,只是在jdk 內部使用,不建議開發者使用;
2、value 表示實際值,可以看到get 方法實際是根據value 是否等於0來判斷布爾值的,這裡的value 定義為volatile,因為volatile 可以保證內存可見性,也就是value 值只要發生變化,其他線程是馬上可以看到變化後的值的;下一篇會講一下volatile 可見性問題,歡迎關注
3、valueOffset 是value 值的內存偏移量,用unsafe.objectFieldOffset 方法獲得,用作後面的compareAndSet 方法;
4、compareAndSet 方法,這就是實現CAS 的核心方法了,在使用AtomicBoolean 的這個方法時,只需要傳遞期望值和待更新的值即可,而它裡面調用了unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是個native 方法,用c++ 實現,具體的代碼就不貼了,總之是利用了CPU 的cmpxchg 指令完成比較並替換,當然根據具體的系統版本不同,實現起來也有所區別,感興趣的可以自行搜一下相關文章。
使用場景
比如AtomicBoolean 可以用在這樣一個場景下,系統需要根據一個布爾變量的狀態屬性來判斷是否需要執行一些初始化操作,如果是多線程的環境下,避免多次重複執行,可以使用AtomicBoolean 來實現,偽代碼如下:
private final static AtomicBoolean flag = new AtomicBoolean(); if(flag.compareAndSet(false,true)){ init(); }比如AtomicInteger 可以用在計數器中,多線程環境中,保證計數準確。
ABA問題
CAS 存在一個問題,就是一個值從A 變為B ,又從B 變回了A,這種情況下,CAS 會認為值沒有發生過變化,但實際上是有變化的。對此,並發包下倒是有AtomicStampedReference 提供了根據版本號判斷的實現,可以解決一部分問題。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。