JAVA的CAS機制

一、什麼是CAS


     CAS即比較並替換,是一種輕量級鎖,一般用於併發量不大的場景,CAS機制中用了3個變量:內存值V,舊的預期值A,要修改的新值B;只有當內存中的值和舊的預期值相等的情況下才更新值爲B,否則該線程會一直自旋等待,下面我們用大白話來解釋CAS。

二、CAS 應用


       想象一下假如現在我們有2個線程,對共享變量進行i++操作,如果不加鎖會出現什麼情景呢,先看一個代碼

  

	private static int count = 0;
	
	public static void main(String[] args) {
		CountDownLatch latch = new CountDownLatch(2);
		for(int i=0;i<2;i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					for(int i=0;i<100;i++) {
						count++;
					}
					latch.countDown();
				}
			}).start();
		}
		try {
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("count=" + count);
	}
     這份代碼理想情況下,按照預期會輸出count=200,但是大多數情況下它的輸出都小於200,這是因爲i++操作不是原子的,這個時候我們可能會想到加鎖,先來看看加synchronized
public class JAVA的CAS機制 {
	
	private static int count = 0;

	public static void main(String[] args) {
		CountDownLatch latch = new CountDownLatch(2);
		for(int i=0;i<2;i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					synchronized (JAVA的CAS機制.class) {
						for(int i=0;i<100;i++) {
							count++;
						}
					}
					latch.countDown();
				}
			}).start();
		}
		try {
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("count=" + count);
	}
}
     這份代碼會輸出count=200,因爲我們通過synchronized來保證每次只有一個線程對共享變量進行操作,雖然synchronized可以解決第一份代碼的問題,但是synchronized相當於一個悲觀鎖,每次只允許一個線程操作。那我們這裏要說的CAS是怎麼用呢,它是怎麼解決第一份代碼的問題呢,我們先看一段代碼

    

private static AtomicInteger count = new AtomicInteger(0);

	public static void main(String[] args) {
		CountDownLatch latch = new CountDownLatch(2);
		for (int i = 0; i < 2; i++) {
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					for (int i = 0; i < 100; i++) {
						count.getAndIncrement();
					}
					latch.countDown();
				}
			}).start();
		}
		try {
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("count=" + count.get());
	}

     這份代碼輸出一直都是count=200,這裏使用了JAVA原子包裏面提供的類atomicInteger,這是一個原子操作類,如果大家感興趣可以看看它的源代碼,底層就是使用了CAS機制來實現,CAS相當於synchronized的輕量級實現,是一個樂觀鎖,假如某個線程沒有更新到值,那麼它會一直自旋並不斷重試,直到最後更新值。在併發量不大的時候,建議大家使用CAS。

三、AtomicInteger源碼分析

     JAVA的CAS機制大量用在了JAVA併發原子類裏面,這裏我們以AtomicInteger類來分析其具體實現

     先來看定義


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;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * Creates a new AtomicInteger with initial value {@code 0}.
     */
    public AtomicInteger() {
    }

    /**
     * Gets the current value.
     *
     * @return the current value
     */
    public final int get() {
        return value;
    }

    /**
     * Sets to the given value.
     *
     * @param newValue the new value
     */
    public final void set(int newValue) {
        value = newValue;
    }
     其中Unsafe類是CAS的核心實現類,JAVA通過它來訪問本地方法;而valueOffset則是變量在內存中的偏移地址,
;而用valatile修飾得變量value保證了多線程內存可見性。

     再看看getAndIncrement

   

  /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
      調用unsafe.getAndInt,假設當前內存中變量值爲2,同時有2個線程來操作這個變量,假設A線程拿到了這個變量,準備修改爲3,B線程也準備修改這個變量,發現目前的值3和自己手裏的變量副本2不一樣,所以更新失敗,這就相當於使用了CAS

 四、CAS的問題

    4.1  CPU消耗大

          CAS線程會不停自旋,如果併發量大的話,將會不停重試,還不釋放CPU,極端情況下會耗光資源。

    4.2  只能保證某個變量的原子操作

        從第三份代碼我們看到,CAS只能保證單個變量的原子性,不能保證某個代碼塊或者某個對象的原子性(這裏可以用併發包裏面的原子更新類AtomicReference實現)

    4.3  經典的ABA問題

       就是說變量A在某個線程修改後成B後,另一個線程又將其修改爲A,導致其他線程誤認爲這個變量實際沒有變過,這就是經典的ABA問題,要解決這個,我們可以加一個版本號,每次比較的時候,不僅需要比較值,還需要看版本號是否一致,一致才修改,在JAVA併發包裏面AtomicStampedReference類就是用了版本號來解決ABA問題。

發佈了40 篇原創文章 · 獲贊 36 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章