1. 程式人生 > >《Java併發程式設計實踐》筆記7——非阻塞同步演算法

《Java併發程式設計實踐》筆記7——非阻塞同步演算法

1.鎖的劣勢:
鎖是實現執行緒同步最簡單的方式,也是代價最高的方式,其有如下的缺點:
(1).重量級:
現代JVM對非競爭的鎖的獲取和釋放進行優化,可以根據系統中鎖佔用的歷史資料決定使用自旋還是掛起等待,使得它非常高效。但是如果有多個執行緒同時請求鎖,JVM就需要向作業系統尋求幫助,沒有獲取到鎖的執行緒可能會被掛起等待,並稍後恢復執行。執行緒的掛起和恢復會帶來很大的上下文切換和排程延時開銷。
(2).優先順序倒置:
當一個執行緒在等待鎖時,它不能做任何其他事情,若一個執行緒在持有鎖的情況下發生了延遲(頁面錯誤、排程延遲等),那麼其他需要該鎖的執行緒都不能前進了,若被阻塞的執行緒是高優先順序執行緒,持有鎖的執行緒優先順序較低,那麼高優先順序的執行緒仍然需要等待低優先順序執行緒釋放鎖,導致優先順序降低,這就是優先順序倒置問題。
(3).悲觀性:
鎖預設是獨佔鎖(讀-寫鎖的讀鎖除外),它是一種悲觀鎖,它假設最壞的情況,並且只有在確保其它執行緒不會造成干擾的情況下執行,會導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖。
2.Volatile的侷限性:
與鎖相比,volatile變數是一種更輕量級的同步機制,因為在使用這些變數時不會發生上下文切換和執行緒排程等操作,但是volatile變數也存在一些侷限:volatile只能保證變數對各個執行緒的可見性,不能用於構建原子的複合操作,即不能保證原子性。因此當一個變數依賴舊值時就不能使用volatile變數。
3.非阻塞同步演算法的優勢:
(1).樂觀性:
非阻塞同步演算法是一種樂觀鎖,它基於衝突檢測,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。
(2).硬體支援:
非阻塞同步演算法底層使用原子化的機器指令(比如比較並交換,compare-and-set)取代鎖,從而保證資料在併發訪問下的一致性。
在大多數處理器架構,包括IA32、Space中採用的都是CAS指令(PowerPc使用的載入連結/儲存條件指令,load-linked/store-conditional),CAS有3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當V符合舊預期值A時,CAS用新值B原子化地更新V的值,否則什麼都不做。CAS的語義是“我認為V的值應該為A,如果是,那麼將V的值更新為B,否則不修改並告訴V的值實際為多少”,CAS是項樂觀鎖技術,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
以synchronized和int模擬CAS演算法,實現類似AtomicInteger的原子自增演算法,例子程式碼如下:

public class SimulatedCAS {
    private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized boolean compareAndSet(int expectedValue, int newValue) {
        return (expectedValue == compareAndSwap(expectedValue, newValue));
    }

    private synchronized int
compareAndSwap(int expectedValue, int newValue) { int oldValue = value; if (oldValue == expectedValue) { value = newValue; } return oldValue; } } public class CasCounter { private SimulatedCAS value; public int getValue() { return value
.get(); } public int increment() { int v; do { v = value.get(); } while (!value.compareAndSet(v, v + 1)); return v + 1; } }

(3).非阻塞且鎖自由:
非阻塞是指一個執行緒的失敗或掛起不應該影響其他行程的失敗或掛起;鎖自由是指演算法的每一步驟中都有一些執行緒能夠繼續執行。
基於CAS的非阻塞同步演算法即是非阻塞又是鎖自由的,非競爭的CAS總能成功,如果多個執行緒競爭一個CAS,總會有一個執行緒勝出並前進,因此非阻塞演算法對於死鎖和優先順序倒置問題具有免疫性(可能會有飢餓或活鎖問題,因為允許重試)。
4.非阻塞演算法與鎖的選擇:
非阻塞演算法與鎖相比,非阻塞演算法的設計和實現複雜度高,可伸縮性和活躍度高,同時對死鎖和優先順序倒置問題具有免疫性(可能會有飢餓或活鎖問題)。
類似於資料庫的樂觀鎖和悲觀鎖,大部分情況下非阻塞演算法的效能高於鎖,但是在激烈競爭條件下鎖效能勝過非阻塞演算法。
5.JDK中對CAS非阻塞演算法的支援:
JDK1.5引入的原子類變數中,如java.util.concurrent.atomic中的AtomicXxx,都使用了底層JVM支援的非阻塞原子化的機器指令,為數字型別的引用型別提供一種高效的CAS操作(compareAndSet方法),而在java.util.concurrent中的大多數類在實現時都直接或間接的使用了這些原子變數類。
使用原子引用AtomicReference的CAS演算法實現一個非阻塞棧,例子程式碼如下:

public class ConcurrentStack<E> {
    AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();

    public void push(E item) {
        Node<E> newHead = new Node<E>(item);
        Node<E> oldHead;
        do {
            oldHead = top.get();
            newHead.next = oldHead;
        } while (!top.compareAndSet(oldHead, newHead));
    }

    public E pop() {
        Node<E> newHead;
        Node<E> oldHead;
        do {
            oldHead = top.get();
            if (oldHead == null) {
                return null;
            }
            newHead = oldHead.next;
        } while (!top.compareAndSet(oldHead, newHead));
        return oldHead.item;
    }

    private static class Node<E> {
        public final E item;
        public Node<E> next;

        public Node(E item) {
            this.item = item;
        }
    }
}

6.CAS中的ABA問題:
一般的CAS在決定是否要修改某個變數時,會判斷一下當前值跟舊值是否相等。如果相等,則認為變數未被其他執行緒修改,可以改。 
但是,“相等”並不真的意味著“未被修改”。 另一個執行緒可能會把變數的值從A改成B,又從B改回成A,這就是ABA問題。如果在演算法中的節點可以被迴圈使用,那麼在使用“比較並交換”指令時就可能出現ABA問題(如果在沒有垃圾回收機制的環境中)。
很多情況下,ABA問題不會影響你的業務邏輯因此可以忽略。但有時不能忽略,這時要解決這個問題,一般的做法是給變數關聯一個只能遞增、不能遞減的版本號。在比較時不但比較變數值,還要再比較一下版本號,JDK的AtomicStampedReference和AtomicMarkableReference類就是使用版本號來解決ABA問題的。