天天看點

Java并發之無鎖Java并發程式設計之無鎖

Java并發程式設計之無鎖

在談論無所之前先來看看樂觀派和悲觀派。對于樂觀派而言,他們總認為事情總會朝着好的方向發展,總認為幾乎不會壞事,我已可以随意的去做事。但是對于悲觀派來說,他們認為出錯是一種常态,是以無論事情大小都會考慮的面面俱到,滴水不漏。

在兩種派别對應在并發中就是加鎖和無鎖,也就是說加鎖是一種悲觀的政策,而無鎖是一種樂觀的政策。對于鎖,如果有多個線程同僚通路一個臨界資源,甯可犧牲性能讓線程等待,也不會讓一個線程不加鎖通路臨界資源。對于無鎖,它會假定對資源的通路是沒有沖突的。也就是多個線程對臨界資源的通路是沒有沖突的。既然沒有沖突,那麼就不需要等待,所有的線程都可以不需要等待的執行下去。如果遇到了沖突,怎麼辦?這裡無鎖政策使用了一種稱為CAS的技術來保證線程執行的安全性。下面我們來具體讨論一下CAS。

無鎖解決沖突的辦法:CAS

CAS的全稱是Compare And Swap即比較和交換。

CAS算法的過程是這樣的:它包含三個參數的變量CAS(V, E, N)。作用如下:

  • V 表示要更新的值。
  • E 表示預期的值。
  • N 表示新值。

僅當V值等于E值的,才會将V值設定為N值,如果V值和E值不同,則說明已經有其他線程做了更新,目前線程什麼都不做。CVS傳回的是目前V的真實值。

CAS是樂觀派,總認為自己可以操作成功。當多個線程同時使用CAS來給一個變量設定時,隻有一個會成功,其它的都會失敗,但是CAS很樂觀,失敗了就失敗了,可以再次嘗試。

Java中的指針:Unsafe類

What? Java中也有指針。Unsfae類就像它的名字一樣,不安全,裡面有一些向C語言的指針一樣直接操作記憶體的方法。并且官方也不推薦直接使用Unsafe類。但是CAS的實作用到了這個類。

下面來看看Unsafe中的一些方法:

// 配置設定記憶體
public native long allocateMemory(long var1);

// 重新配置設定記憶體
public native long reallocateMemory(long var1, long var3);

// 拷貝記憶體
public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);

// 釋放記憶體
public native void freeMemory(long var1);

// 擷取起始位址
public native long getAddress(long var1);


// 擷取作業系統記憶體頁大小
public native int pageSize();

.....
           

Unsafe中有兩個方法可以将線程挂起和恢複,如下:

// 線程調用方法,将線程挂起
public native void unpark(Object var1);

// 線程恢複
public native void park(boolean var1, long var2);
           

Unsafe中的關于CAS的操作:

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

CAS的操作的核心實作是這三個方法。

三個方法的參數都類似,分别為:

  1. CAS需要更改變量的對象;
  2. 對象記憶體的偏移量;
  3. 期望值
  4. 需要設定的值

其中偏移量可以通過Unsafe類中的objectFieldOffset()方法擷取到,這些方法如下:

因為int,long,boolean類型的相關操作不是原子性的,是以JDK在1.5之後提供了atomic包(具體在

java.util.concurrent.atomic

中)來将這些操作變成原子操作。

下面的圖檔中的是atomic下提供的原子操作的類:

Java并發之無鎖Java并發程式設計之無鎖

這裡以AtomicInteger來進行分析:

public class AtomicInteger extends Number implements java.io.Serializable {
    
    // 執行個體化Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    
    // valueOffset變量儲存的是value中的偏移量,這一點可以在下面的static初始化塊中可以看出
    private static final long valueOffset;

    static {
        try {
            // 擷取value的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

    /**
     * 通過int值執行個體化一個AtomicInteger對象
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * 執行個體化一個AtomicInteger對象,其初始值為0
     */
    public AtomicInteger() {
    }
    
    // 擷取目前值
    public final int get() {
        return value;
    }
    // 設定目前值
    public final void set(int newValue) {
        value = newValue;
    }

    // 延遲設值
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }

    // 設定新值擷取舊值
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    // CAS操作,把目前值和預期值做比較,相當時設定新值
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    
    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    // 目前值加1,傳回舊值
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    // 目前值-1,傳回舊值
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }

    // 目前值加上delta,然後傳回舊值
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    // 目前值加1,然後傳回
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    
    // 目前值減1,傳回傳回
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }

    // 目前值加上delta,然後傳回
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

    // ...
}
           

其中的大部分方法都直接或者間接使用了CAS來保證安全。

除了上面的對于基本變量的Atomic類,還有關于普通對象引用的Atmoic類。

AtomicReference 無鎖的引用

AtomicReference和AtmoicInteger非常類似,不同之處就是AtmoicInteger是對整數的封裝,而AtmoicReference是對普通對象的引用,也就是它可以保證在修改對象引用時線程的安全性。

CAS中有一個很重要的問題ABA。CAS比較的是對象中的值和期望值,但是有可能在你擷取到目前對象的資料後,在準備修改為新值之前,對象的值被其他線程連續修改兩次,而且經過這兩次修改之後,對象有恢複為舊值。這樣,前後的結果看似沒有被改過,但是其實已經被修改了2次。過程如下圖:

Java并發之無鎖Java并發程式設計之無鎖

一般來說,發生這種事情的可能性很小。而且即使發生了也不會有什麼影響,比如,一個數字,被修改一次後,在修改回去,不會對CAS産生什麼影響。

但是有時候在一些具體問題中這種情況就有可能發生。是以在JDK中提供了AtomicStampedReference來解決這種問題。

AtomicStampedReference

AtomicStampedReference在内部維護了一個時間戳。當AtmoicStampedReference對應的數字被修改時,除了更新資料本身,還必須更新時間戳。當AtomicStampedReference設定對象值時,對象和時間戳都必須滿足期望值,才會寫入成功。通過維護時間戳能有效的防止ABA問題。

AtmoicStampedReference的幾個API在Atmomic的基礎上新增了幾個有關時間戳的資訊。

// 參數為:期望值,新值,期望時間戳,新的時間戳
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp);
// 擷取索引
public V getReference()
// 擷取時間戳
public int getStamp()
// 設定目前對象引用和時間戳
public void set(V newReference, int newStamp)