Java併發編程之CAS應用

個人博客請訪問 http://www.x0100.top             

1. CAS 介紹

CAS,Compare And Swap,即比較並交換。Doug lea 大神在同步組件中大量使用 CAS 技術鬼斧神工地實現了 Java 多線程的併發操作。整個 AQS 同步組件、Atomic 原子類操作等等都是以 CAS 實現的。可以說 CAS 是整個 J.U.C 的基石。

CAS 比較交換的過程 CAS(V,A,B):
V-一個內存地址存放的實際值、A-舊的預期值、B-即將更新的值,當且僅當預期值 A 和內存值 V 相同時,將內存值修改爲 B 並返回 true,否則什麼都不做,並返回 false。

CAS VS synchronized

synchronized 是線程獲取鎖是一種悲觀鎖策略,即假設每一次執行臨界區代碼都會產生衝突,所以當前線程獲取到鎖的之後會阻塞其他線程獲取該鎖。

CAS(無鎖操作)是一種樂觀鎖策略,它假設所有線程訪問共享資源的時候不會出現衝突,所以出現衝突時就不會阻塞其他線程的操作,而是重試當前操作直到沒有衝突爲止。

2. 如何用 CAS 解決原子性問題

如下代碼,目的是啓動 10 個線程,每個線程將 a 累加 1000 次,最終得到 a=10000。

public class CASTest {
	public int a = 0;

	public void increase() {
		a++;
	}

	public static void main(String[] args) {
		final CASTest test = new CASTest();
		for (int i = 0; i < 10; i++) {
			new Thread() {
				public void run() {
					for (int j = 0; j < 1000; j++)
						test.increase();
				};
			}.start();
		}

		while (Thread.activeCount() > 1) {
			// 保證前面的線程都執行完
			Thread.yield();
		}
		System.out.println(test.a);
	}
}

結果:每次運行結果都小於 10000。

原因分析:

當線程 1 將 a 加到 2 時,a=2 刷新到主內存;
線程 2 執行增加運算時,到主內存讀取 a=2,此時線程 3 也要執行增加運算,也到主內存中讀取到 a=2;
線程 2 和線程 3 執行的都是 a=2+1,將 a=3 刷新到主內存。
相當於兩次加 1 運算只將 a 增加了 1,也就是說存在執行了多次加 1 運算卻只是將 a 增加 1 的情況,所以 10000 次加 1 運算,得到的結果會小於 10000。

原子性問題,解決方案 synchronized 和 CAS。

解決方案一:synchronized 加鎖

public synchronized void increase() {
    a++;
}

通過 synchronized 加鎖之後,每次只能有一個線程訪問 increase()方法,能夠保證最終得到 10000。但是 synchronized 加鎖是個重量級操作,程序執行效率很低。

解決方案二:CAS

public AtomicInteger a = new AtomicInteger();
public void increase() {
    a.getAndIncrement();
}

利用 CAS,保證 a=a+1 是原子性操作,最終得到結果 10000。

3. CAS 原理

探究 CAS 原理,其實就是探究上個例子中 a.getAndIncrement()如何保證 a=a+1 是原子性操作,先通過源碼看下。

AtomicInteger 類結構

public class AtomicInteger extends Number implements java.io.Serializable {
    // 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;
}
  1. Unsafe 是 CAS 的核心類,由於 Java 方法無法直接訪問底層系統,需要通過本地(native)方法來訪問,Unsafe 相當於一個後門,基於該類可以直接操作特定內存的數據。

  2. 變量 valueOffset 表示該變量值在內存中的偏移地址,因爲 Unsafe 就是根據內存偏移地址獲取數據的原值。

  3. 變量 value 用 volatile 修飾,保證了多線程之間的內存可見性。

a.getAndIncrement()的實現如下

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

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;
}

getIntVolatile(var1, var2):根據對象 var1 和對象中該變量地址 var2,獲取變量的值 var5。

this.compareAndSwapInt(var1, var2, var5, var5 + var4);

  1. 根據對象 var1 和對象中該變量地址 var2 獲取變量當前的值 value

  2. 比較 value 跟 var5,如果 value==var5,則 value=var5+var4 並返回 true。這步操作就是比較和替換操作,是原子性的

  3. 如果 value!=var5,則返回 false,再去自旋循環到下一次調用 compareAndSwapInt 方法。

可見,getAndIncrement()的原子性是通過 compareAndSwapInt()中的第二步比較和替換保證的,那麼 compareAndSwapInt()又是怎麼保證原子性的呢?

compareAndSwapInt 方法是 JNI(Java Native InterfaceJAVA 本地調用),java 通過 C 來調用 CPU 底層指令實現的。

compareAndSwapInt 方法中的比較替換操作之前插入一個 lock 前綴指令,這個指令能過確保後續操作的原子性。

lock 前綴指令確保後續指令執行的原子性:

在Pentium及之前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其它處理器暫時無法通過總線訪問內存,很顯然,這個開銷很大。
在新的處理器中,Intel使用緩存鎖定來保證指令執行的原子性,緩存鎖定將大大降低lock前綴指令的執行開銷。

CPU 提供了兩種方法來實現多處理器的原子操作:總線加鎖和緩存加鎖。

  • 總線加鎖:總線加鎖就是就是使用處理器提供的一個 LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔使用共享內存。但是這種處理方式顯得有點兒霸道,不厚道,他把 CPU 和內存之間的通信鎖住了,在鎖定期間,其他處理器都不能其他內存地址的數據,其開銷有點兒大。所以就有了緩存加鎖。

  • 緩存加鎖:其實針對於上面那種情況我們只需要保證在同一時刻對某個內存地址的操作是原子性的即可。緩存加鎖就是緩存在內存區域的數據如果在加鎖期間,當它執行鎖操作寫回內存時,處理器不在輸出 LOCK#信號,而是修改內部的內存地址,利用緩存一致性協議來保證原子性。緩存一致性機制可以保證同一個內存區域的數據僅能被一個處理器修改,也就是說當 CPU1 修改緩存行中的 i 時使用緩存鎖定,那麼 CPU2 就不能同時緩存了 i 的緩存行。

比較替換操作過程分析

  1. 假設線程 A 和線程 B 同時調用 a.getAndIncrement()-->getAndIncrement()-->getAndAddInt(),AtomicInteger 裏面的 value 原始值爲 3,即主內存中 AtomicInteger 的 value 爲 3,且線程 A 和線程 B 各自持有一份 value 的副本,值爲 3。

  2. 線程 A 通過 getIntVolatile(var1, var2)拿到 value 值 3,線程 B 也通過 getIntVolatile(var1, var2)拿到 value 值 3,線程 A 和線程 B 同時調用 compareAndSwapInt()。

  3. 線程 A 執行 compareAndSwapInt()方法比較和替換時,其他 CPU 無法訪問該變量的內存,所以線程 B 不能進行比較替換。線程 A 成功修改內存值爲 4,返回 true,執行結束。

  4. 線程 B 恢復,執行 compareAndSwapInt()方法比較和替換,發現內存的實際值 4 跟自己期望值 3 不一致,說明該值已經被其它線程提前修改過了,返回 false,自旋進入 while 循環,再通過 getIntVolatile(var1, var2)方法獲取 value 值 4,執行 compareAndSwapInt()比較替換,直到成功。

4. CAS 的問題

ABA 問題

CAS 需要檢查操作值有沒有發生改變,如果沒有發生改變則更新。但是存在這樣一種情況:如果一個值原來是 A,變成了 B,然後又變成了 A,那麼在 CAS 檢查的時候會認爲沒有改變,但是實質上它已經發生了改變,這就是 ABA 問題。

解決方案可以沿襲數據庫中常用的樂觀鎖方式,添加一個版本號可以解決。原來的變化路徑 A->B->A 就變成了 1A->2B->3A。

在 java 1.5 後的 atomic 包中提供了 AtomicStampedReference 來解決 ABA 問題,解決思路就是這樣的。

自旋時間過長

使用 CAS 時非阻塞同步,也就是說不會將線程掛起,會自旋(無非就是一個死循環)進行下一次嘗試,如果自旋 CAS 長時間地不成功,則會給 CPU 帶來非常大的開銷。

優化:限制 CAS 自旋的次數,例如 BlockingQueue 的 SynchronousQueue。

只能保證一個共享變量的原子操作

當對一個共享變量執行操作時 CAS 能保證其原子性,如果對多個共享變量進行操作,CAS 就不能保證其原子性。

解決方案:把多個變量整成一個變量

  1. 利用對象整合多個共享變量,即一個類中的成員變量就是這幾個共享變量,然後將這個對象做 CAS 操作就可以保證其原子性。atomic 中提供了 AtomicReference 來保證引用對象之間的原子性。

  2. 利用變量的高低位,如 JDK 讀寫鎖 ReentrantReadWriteLock 的 state,高 16 位用於共享模式 ReadLock,低 16 位用於獨佔模式 WriteLock。

5. Java 中的原子操作類

在 J.U.C 下的 atomic 包提供了一系列原子操作類。

1)基本數據類型的原子操作類

AtomicInteger、AtomicLong、AtomicBoolean

以 AtomicInteger 爲例總結一下常用的方法:

addAndGet(int delta) :以原子方式將輸入的數值delta與實例中原本的值相加,並返回最後的結果;
incrementAndGet() :以原子的方式將實例中的原值進行加1操作,並返回最終相加後的結果;
getAndSet(int newValue):將實例中的值更新爲新值newValue,並返回舊值;
getAndIncrement():以原子的方式將實例中的原值加1,返回的是自增前的舊值;

用法:

public class AtomicDemo {
    private static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) {
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.get());
    }
}

輸出結果:

1
2

2)數組類型的原子操作類

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray(引用類型數組)。

以 AtomicIntegerArray 來總結下常用的方法:

addAndGet(int i, int delta):以原子更新的方式將數組中索引爲i的元素與輸入值delta相加;
getAndIncrement(int i):以原子更新的方式將數組中索引爲i的元素自增加1;
compareAndSet(int i, int expect, int update):將數組中索引爲i的位置的元素進行更新;

用法:

AtomicIntegerArray 與 AtomicInteger 的方法基本一致,只不過在 AtomicIntegerArray 的方法中會多一個指定數組索引位 i。

通過 getAndAdd 方法將位置爲 1 的元素加 5,從結果可以看出索引爲 1 的元素變成了 7,該方法返回的也是相加之前的數爲 2。

public class AtomicDemo {
    private static int[] value = new int[]{1, 2, 3};
    private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //對數組中索引爲1的位置的元素加5
        int result = integerArray.getAndAdd(1, 5);
        System.out.println(integerArray.get(1));
        System.out.println(result);
    }
}

輸出結果:

7
2

3)引用類型的原子操作類

AtomicReference

用法:

public class AtomicDemo {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user);
        System.out.println(reference.get());
    }

    static class User {
        private String userName;
        private int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

輸出結果:

User{userName='a', age=1}
User{userName='b', age=2}

4)字段類型的原子操作類

如果需要更新對象的某個字段,並在多線程的情況下,能夠保證線程安全,atomic 同樣也提供了相應的原子操作類:

AtomicIntegeFieldUpdater:原子更新整型字段類;
AtomicLongFieldUpdater:原子更新長整型字段類;
AtomicStampedReference:原子更新引用類型,這種更新方式會帶有版本號。解決CAS的ABA問題。

用法:

  1. 通過 AtomicIntegerFieldUpdater 的靜態方法 newUpdater 來創建一個更新器,並且需要設置想要更新的類和屬性;

  2. 更新類的屬性必須使用 public volatile 進行修飾;

public class AtomicDemo {

    private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
    public static void main(String[] args) {
        User user = new User("a", 1);
        int oldValue = updater.getAndAdd(user, 5);
        System.out.println(oldValue);
        System.out.println(updater.get(user));
    }

    static class User {
        private String userName;
        public volatile int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

輸出結果:

1
6

總結

CAS 即比較和替換,可以高效的解決原子性問題。

CAS 原子操作原理:使用一個期望值和一個變量的當前值進行比較,如果當前變量的值與我們期望的值相等,就使用一個新值替換當前變量的值。

Java 中的 CAS:atomic 包下原子操作類,如 AtomicInteger 常用於修飾共享變量來保證原子性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章