1. 程式人生 > >Java多執行緒程式設計-(14)-無鎖CAS操作以及Java中Atomic併發包的“18羅漢”

Java多執行緒程式設計-(14)-無鎖CAS操作以及Java中Atomic併發包的“18羅漢”

原文出自 : https://blog.csdn.net/xlgen157387/article/details/78364246



上一篇:

Java多執行緒程式設計-(13)- 關於鎖優化的幾點建議

一、背景

通過上面的學習,我們應該很清楚的知道了在多執行緒併發情況下如何保證資料的安全性和一致性的兩種主要方法:一種是加鎖,另一種是使用ThreadLocal。鎖是一種以時間換空間的方式,而ThreadLocal是一種以空間換時間的方式

以上的內容一個是有鎖操作,另一個是ThreadLocal的操作,那麼是否有一種不使用鎖就可以實現多執行緒的併發那?答案是有!下邊我們一點點介紹什麼是無鎖,以及無鎖的常用類。

二、無鎖

我們知道在進行執行緒切換的時候是需要進行上下文切換的,意思就是在切換執行緒的時候會儲存上一任務的狀態,以便下次切換回這個任務時,可以再載入這個任務的狀態。所以任務從儲存到再載入的過程就是一次上下文切換

上述說的上下文切換也就是我們說的執行緒切換的時候所花費的時間和資源開銷。因此,如何減少上下文切換是一種可以提高多執行緒併發效率的有效方案。這裡的無鎖正是一種減少上下文切換的技術。

對於併發控制而言,鎖是一種悲觀的策略。它總是假設每一次的臨界區操作會產生衝突,因此,必須對每次操作都小心翼翼。如果有多個執行緒同時需要訪問臨界區資源,就寧可犧牲效能讓執行緒進行等待,所以說鎖會阻塞執行緒執行。

而無鎖是一種樂觀的策略,它會假設對資源的訪問是沒有衝突的。既然沒有衝突,自然不需要等待,所以所有的執行緒都可以在不停頓的狀態下持續執行。

那遇到衝突怎麼辦呢?無鎖的策略使用一種叫做比較交換的技術(CAS Compare And Swap)來鑑別執行緒衝突,一旦檢測到衝突產生,就重試當前操作直到沒有衝突為止

三、什麼是比較交換(CAS)

(1)與鎖相比,使用比較交換(下文簡稱CAS)會使程式看起來更加複雜一些。但由於其非阻塞性,它對死鎖問題天生免疫,並且,執行緒間的相互影響也遠遠比基於鎖的方式要小。更為重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有執行緒間頻繁排程帶來的開銷,因此,它要比基於鎖的方式擁有更優越的效能。

(2)無鎖的好處:

第一,在高併發的情況下,它比有鎖的程式擁有更好的效能;
第二,它天生就是死鎖免疫的。

  
  • 1
  • 2

就憑藉這兩個優勢,就值得我們冒險嘗試使用無鎖的併發。

(3)CAS演算法的過程是這樣:它包含三個引數CAS(V,E,N): V表示要更新的變數,E表示預期值,N表示新值。僅當V值等於E值時,才會將V的值設為N,如果V值和E值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。最後,CAS返回當前V的真實值。

(4)CAS操作是抱著樂觀的態度進行的,它總是認為自己可以成功完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS操作即使沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並進行恰當的處理。

(5)簡單地說,CAS需要你額外給出一個期望值,也就是你認為這個變數現在應該是什麼樣子的。如果變數不是你想象的那樣,那說明它已經被別人修改過了。你就重新讀取,再次嘗試修改就好了。

(6)在硬體層面,大部分的現代處理器都已經支援原子化的CAS指令。在JDK 5.0以後,虛擬機器便可以使用這個指令來實現併發操作和併發資料結構,並且,這種操作在虛擬機器中可以說是無處不在。

三、Java中的原子操作類

Java中的原子操作類大致可以分為4類:原子更新基本型別、原子更新陣列型別、原子更新引用型別、原子更新屬性型別。這些原子類中都是用了無鎖的概念,有的地方直接使用CAS操作的執行緒安全的型別。

JDK 1.7.9版本java.util.concurrent.atomic包如下:

這裡寫圖片描述

分類如下:

這裡寫圖片描述

四、原子更新基本型別

  1. AtomicBoolean:原子更新布林型別;
  2. AtomicInteger:原子更新整數型別;
  3. AtomicLong:原子更新長整型型別;

三個的基本原理大致一樣,這裡討論AtomicInteger,方法和屬性如下:

這裡寫圖片描述

這個的每一個方法根據方法名可以瞭解其大致意思,不在這裡贅述,看一個案例,產生10000個整數並輸出:

public class AtomicIntegerDemo {
private static AtomicInteger integer = new AtomicInteger();

public static void main(String[] args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.println(integer.incrementAndGet());
            }
        }
    }).start();
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

接下來看一下incrementAndGet() 這個方法的實現:

這裡寫圖片描述

int current = get();

public final int get() {
return value;
}

private volatile int value;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

首先獲取當前的值,這裡的get方法呼叫結果返回一個volatile 修飾的value值,這樣的話,上面正在訪問的執行緒可以發現其他執行緒對臨界區資料的修改,volatile實現了JMM中的可見性。使得對臨界區資源的修改可以馬上被其他執行緒看到。

int next = current + 1;

  
  • 1

這一行程式碼得到的結果就是需要更新的值,也就是需要對原來的值進行加1操作。

if (compareAndSet(current, next))
	return next;

  
  • 1
  • 2

這一行程式碼就是呼叫了CAS方法進行原子更新操作的,符合CAS的設計原理,意思就是在設定值的時候,首先判斷一下是否和預期的值一樣,如果一樣則修改,不一樣的話就表示修改失敗,而這裡最外層是for (;;) 也就是一個死迴圈,這是因為在CAS無鎖的情況下我們的修改可能會失敗,這樣的話通過這個死迴圈就可以繼續迴圈知道成功修改位置,這也是實現CAS的關鍵。

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

private static final Unsafe unsafe = Unsafe.getUnsafe();

  • 1
  • 2
  • 3
  • 4
  • 5

這裡的compareAndSet() 方法呼叫的是Unsafe的compareAndSwapInt() 方法,Unsafe類是CAS實現的核心。

從名字可知,這個類標記為不安全的,它本質上可以理解為是Java中的指標,Unsafe封裝了一下不安全的操作,這是因為指標是不安全的,不正確的使用可能會造成意想不到的結果,因此JDK作者不希望使用者使用這個類,只可以在JDK內部使用到。Atomic包裡的類基本都是使用Unsafe這個類實現的

Unsafe提供了3種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);

  • 1
  • 2
  • 3
  • 4
  • 5

五、原子更新引用型別

  1. AtomicReference:原子更新引用型別;
  2. AtomicStampedReference:原子更新帶有版本號的引用型別;
  3. AtomicMarkableReference:原子更新帶有標記位的引用型別。可以原子更新一個布林型別的標記為和引用型別;

(1)AtomicReference

AtomicReference是對普通的物件的引用,可以保證我們在修改物件應用的時候保證執行緒的安全性,舉例如下:

public class AtomicReferenceDemo {
public static AtomicReference<User> atomicReference =
        new AtomicReference<User>();

public static void main(String[] args) {
    User user = new User("xuliugen", "123456");
    atomicReference.set(user);

    User updateUser = new User("Allen", "654321");
    atomicReference.compareAndSet(user, updateUser);
    System.out.println(atomicReference.get().getUserName());
    System.out.println(atomicReference.get().getUserPwd());
}

static class User {
    private String userName;
    private String userPwd;
    //省略get、set、構造方法
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

這是一個簡單的使用,但是有一個情況是需要注意的,因為在每次compareAndSet 的時候,假如我們預期的值被別的執行緒修改了,然後在又被其他執行緒修改會原來的狀態了,如下圖:

這裡寫圖片描述

他不像操作AtomicInteger等一樣,即使中間被修改,但是他是沒有狀態的,最後的記過不會受到影響,道理很簡單,就是我們數學中的等式替換,但是對於AtomicReference 這種狀態的遷移可能是一種災難!

(2)表示AtomicReference狀態的例項

假設有一家咖啡店,為每一位會員卡餘額小於20的會員一次性充值20元,以刺激消費。條件是隻充值一次!

public class AtomicReferenceStateDemo {
//設定預設餘額為19,表示這是一個需要被充值的賬戶
private static AtomicReference<Integer> money =
        new AtomicReference<Integer>(19);

public static void main(String[] args) {

    //<a href="https://www.baidu.com/s?wd=%E6%A8%A1%E6%8B%9F&amp;tn=24004469_oem_dg&amp;rsv_dl=gh_pl_sl_csd" target="_blank">模擬</a>多個執行緒同時為使用者的賬戶充值
    for (int i = 0; i &lt; 200; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) { //CAS模式中的死迴圈,保證更新成功
                    Integer m = money.get();
                    if (m &lt; 20) {
                        if (money.compareAndSet(m, m + 20)) {
                            System.out.println("餘額小於20,充值成功,餘額為:"
                                    + money.get() + "元!");
                            break;
                        }
                    } else {
                        //System.out.println("餘額大於20,無需充值!");
                        break;
                    }
                }
            }
        }, "rechargeThread" + i).start();
    }

    new Thread(new Runnable() {
        @Override
        public void run() {
            //<a href="https://www.baidu.com/s?wd=%E6%A8%A1%E6%8B%9F&amp;tn=24004469_oem_dg&amp;rsv_dl=gh_pl_sl_csd" target="_blank">模擬</a>多次消費
            for (int i = 0; i &lt; 200; i++) {
                while (true) {
                    Integer m = money.get();
                    if (m &gt; 10) {
                        System.out.println("大於<a href="https://www.baidu.com/s?wd=10%E5%85%83&amp;tn=24004469_oem_dg&amp;rsv_dl=gh_pl_sl_csd" target="_blank">10元</a>,可以進行消費!");
                        if (money.compareAndSet(m, m - 10)) {
                            System.out.println("消費成功,餘額為:" + money.get());
                            break;
                        }
                    } else {
                        //System.out.println("沒有足夠的餘額,無法進行消費!");
                        break;
                    }
                }
            }
        }
    }, "userConsumeThread").start();

}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

執行結果:

這裡寫圖片描述

可以看出在賬戶充值的時候,會員可能正在消費,由於在充值的時候,判斷的是賬戶餘額是否小於20,如果是則進行充值,但是沒有考慮到如何只充值一次的情況,因為他只是比較預期的值是否小於20,而無法判斷該值的狀態,所以賬戶被多次充值了,這就是因為AtomicReference無法表達狀態的遷移!

(3)AtomicStampedReference帶有時間戳的物件引用型別

為了表述一個有狀態遷移的AtomicReference而升級為帶有時間戳的物件引用AtomicStampedReferenceAtomicStampedReference 解決了上述物件在修改過程中,丟失狀態資訊的問題,使得物件的值不僅與預期的值相比較,還通過時間戳進行比較,這就可以很好的解決物件被反覆修改導致執行緒無法正確判斷物件狀態的問題。

AtomicStampedReference 更新值的時候還必須要更新時間戳,只有當值滿足預期且時間戳滿足預期的時候,寫才會成功!

把上述的程式碼改成使用AtomicStampedReference 的方式如下:

public class AtomicStampedReferenceDemo {
//設定預設餘額為19,表示這是一個需要被充值的賬戶,初始化時間戳為0
private static AtomicStampedReference&lt;Integer&gt; money =
        new AtomicStampedReference&lt;Integer&gt;(19, 0);

public static void main(String[] args) {

    //<a href="https://www.baidu.com/s?wd=%E6%A8%A1%E6%8B%9F&amp;tn=24004469_oem_dg&amp;rsv_dl=gh_pl_sl_csd" target="_blank">模擬</a>多個執行緒同時為使用者的賬戶充值
    for (int i = 0; i &lt; 10; i++) {
        //多個執行緒同時獲取一個預期的時間戳,如果執行緒執行的時候發現和預期值不一樣
        //則表示已經被其他執行緒修改,則無需在充值,保證只充值一次!
        final int timeStamp = money.getStamp();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) { //CAS模式中的死迴圈,保證更新成功
                    Integer m = money.getReference();
                    if (m &lt; 20) {
                        if (money.compareAndSet(m, m + 20, timeStamp, timeStamp + 1)) {
                            System.out.println("餘額小於20,充值成功,餘額為:"
                                    + money.getReference() + "元!");
                            break;
                        }
                    } else {
                        //System.out.println("餘額大於20,無需充值!");
                        break;
                    }
                }
            }
        }, "rechargeThread" + i).start();
    }

    new Thread(new Runnable() {
        @Override
        public void run() {
            //模擬多次消費
            for (int i = 0; i &lt; 10; i++) {
                while (true) {
                    Integer m = money.getReference();
                    int timeStamp = money.getStamp();
                    if (m &gt; 10) {
                        System.out.println("大於<a href="https://www.baidu.com/s?wd=10%E5%85%83&amp;tn=24004469_oem_dg&amp;rsv_dl=gh_pl_sl_csd" target="_blank">10元</a>,可以進行消費!");
                        if (money.compareAndSet(m, m - 10, timeStamp, timeStamp + 1)) {
                            System.out.println("消費成功,餘額為:" + money.getReference());
                            break;
                        }
                    } else {
                        //System.out.println("沒有足夠的餘額,無法進行消費!");
                        break;
                    }
                }
            }
        }
    }, "userConsumeThread").start();
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

執行結果:

這裡寫圖片描述

可以看出只充值一次!

六、原子更新陣列型別

  1. AtomicIntegerArray:原子更新整數型數組裡的元素;
  2. AtomicLongArray:原子更新長整型數組裡的元素;
  3. AtomicReferenceArray:原子更新引用型別數組裡的元素;

簡單例項:

public class AtomicIntegerArrayDemo {
private static int[] value = new int[]{1, 2, 3, 4, 5};
private static AtomicIntegerArray atomic =
        new AtomicIntegerArray(value);

public static void main(String[] args) {
    atomic.getAndSet(2, 100);
    System.out.println(atomic.get(2));
}

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

七、原子更新屬性型別

如果需要原子地更新某個類裡的某個欄位時,就需要使用原子更新欄位值,主要有下邊三個:

  1. AtomicIntegerFieldUpdater:原子更新整數型欄位;
  2. AtomicLongFieldUpdater:原子更新長整型欄位;
  3. AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位;

示例如下:

public class AtomicIntegerFieldUpdaterDemo {
public static AtomicIntegerFieldUpdater atomic =
        AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

public static void main(String[] args) {
    User user = new User("xuliugen", 24);
    System.out.println(atomic.getAndIncrement(user));
    System.out.println(atomic.get(user));
}

static class User {
    private String userName;
    public volatile int age;
    //省略get、set、構造方法
}

}
//輸出結果:
24
25

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20


參考文章:

1、http://www.cnblogs.com/756623607-zhang/p/6876060.html

        </div>