【併發編程】 --- Compare And Swap(CAS)原理分析

源碼地址:https://github.com/nieandsun/concurrent-study.git


1 什麼是CAS?


1.1 加鎖和CAS解決原子性問題的不同原理

首先看如下代碼:

package com.nrsc.ch2.cas;

import java.util.ArrayList;
import java.util.List;

public class CasDemo {

    //共享資源
    static int i = 0;

    public static void increase() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        Runnable r = () -> {
            for (int j = 0; j < 1000; j++) {
                increase();
            }
        };

        List<Thread> threads = new ArrayList<>();
        for (int j = 0; j < 10; j++) {
            Thread thread = new Thread(r);
            threads.add(thread);
            thread.start();
        }

        //確保前面10個線程都走完
        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println(i);
    }
}

相信每個人都知道這段代碼由於i++不是原子操作,因此會導致這10個線程執行後的最終結果不是10*1,000 = 10,000。
當然也相信幾乎所有人都知道通過加鎖可以解決這個問題,加鎖方式解決該問題的原理基本可以用下圖進行概況:
在這裏插入圖片描述
而其實除了加鎖之外利用CAS機制也能解決這個問題。既然說它是除了加鎖之外的另一種解決方式,那它肯定是無鎖的,因此利用CAS機制解決該問題的方式大致可以用下圖進行概況:
在這裏插入圖片描述
那到底啥是CAS呢?它又是是如何解決這個問題的呢?


1.2 CAS原理分析

先看定義(摘自百度百科):

compare and swap,解決多線程並行情況下使用鎖造成性能損耗的一種機制,CAS操作包含三個操作數——內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。否則,處理器不做任何操作。無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認爲位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。"

再畫個圖解釋一下:
在這裏插入圖片描述


2 CAS可能的問題


2.1 ABA問題

說實話剛接觸CAS的時候,其實我就想到ABA這個問題了。☺☺☺

通過1.2可以知道,CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化就更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了 —> 這就是所謂的ABA問題。

舉個通俗點的例子,你倒了一杯水放桌子上,幹了點別的事,然後同事把你水喝了又給你重新倒了一杯水,你回來看水還在,拿起來就喝,如果你不管水中間被人喝過,只關心水還在,還好 ; 但是假若你是一個比較講衛生的人,那你肯定就不高興了。。。

ABA問題的解決思路其實也很簡單,就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A了。


2.2 循環時間長開銷大

自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。


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

當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖

還有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如,有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象裏來進行CAS操作。


3 JDK中對CAS的支持 — Unsafe類

java中提供了對CAS操作的支持,具體在sun.misc.unsafe類中,聲明如下:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上面各個參數的含義:

  • 參數var1:表示要操作的對象
  • 參數var2:表示要操作對象中屬性地址的偏移量
  • 參數var4:表示需要修改數據的期望的值
  • 參數var5/var6:表示雷要修改爲的新值

注意:Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。過度的使 用Unsafe類會使得出錯的機率變大,因此Java官方並不建議使用的,官方文檔也幾乎沒有。Unsafe對 象不能直接調用,只能通過反射獲得。


4 JDK中的相關原子操作類簡介 — 底層CAS機制


4.1 AtomicInteger

  • int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果。
  • boolean compareAndSet(int expect,int update):如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值。
  • int getAndIncrement():以原子方式將當前值加1,注意,這裏返回的是自增前的值。
  • int getAndSet(int newValue):以原子方式設置爲newValue的值,並返回舊值。

4.2 AtomicIntegerArray

主要是提供原子的方式更新數組裏的整型,其常用方法如下。

  • int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加。
  • boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。

需要注意的是,數組value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前數組複製一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組。


4.3更新引用類型

原子更新基本類型的AtomicInteger,只能更新一個變量,如果要原子更新多個變量,就需要使用這個原子更新引用類型提供的類。Atomic包提供了以下3個類。

  • AtomicReference
    原子更新引用類型。
  • AtomicStampedReference
    利用版本戳的形式記錄了每次改變以後的版本號,這樣的話就不會存在ABA問題了。這就是AtomicStampedReference的解決方案。AtomicMarkableReference跟AtomicStampedReference差不多, AtomicStampedReference是使用pair的int stamp作爲計數器使用,AtomicMarkableReference的pair使用的是boolean mark。 還是那個水的例子,AtomicStampedReference可能關心的是動過幾次,AtomicMarkableReference關心的是有沒有被人動過,方法都比較簡單。
  • AtomicMarkableReference:
    原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。

4.4 原子更新字段類

如果需原子地更新某個類裏的某個字段時,就需要使用原子更新字段類,Atomic包提供了以下3個類進行原子字段更新。
要想原子地更新字段類需要兩步。第一步,因爲原子更新字段類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()創建一個更新器,並且需要設置想要更新的類和屬性。 第二步,更新類的字段(屬性)必須使用public volatile修飾符。

  • AtomicIntegerFieldUpdater:
    原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:
    原子更新長整型字段的更新器。
  • AtomicReferenceFieldUpdater:
    原子更新引用類型裏的字段。

5 樂觀鎖和悲觀鎖

悲觀鎖從悲觀的角度出發: (總有刁民想害朕)
總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這
樣別人想拿這個數據就會阻塞。因此synchronized我們也將其稱之爲悲觀鎖。JDK中的ReentrantLock
也是一種悲觀鎖。性能較差!
樂觀鎖從樂觀的角度出發:
總是假設最好的情況,每次去拿數據的時候都認爲別人不會修改,就算改了也沒關係,再重試即可。所
以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去修改這個數據,如何沒有人修改則更
新,如果有人修改則重試。

CAS這種機制也可以將其稱之爲樂觀鎖。綜合性能較好!

CAS獲取共享變量時,爲了保證該變量的可見性,需要使用volatile修飾。結合CAS和volatile可以實現無鎖併發,適用於競爭不激烈、多核 CPU 的場景下。

  • (1)因爲沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一。
  • (2)但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章