1. 程式人生 > >java多線程11.非阻塞同步機制

java多線程11.非阻塞同步機制

!= cte ret 包含 策略 返回 編譯 -- current

關於非阻塞算法CAS。 比較並交換CAS:CAS包含了3個操作數---需要讀寫的內存位置V,進行比較的值A和擬寫入的新值B。當且僅當V的值等於A時,CAS才會通過原子的方式用新值B來更新V的值,否則不會執行任何操作。無論位置V的值是否等於A,都將返回V原有的值。然後線程可以基於新返回的V值來做對應的操作,可以反復嘗試。通常,反復重試是一種合理的策略,但在一些競爭很激烈的情況下,更好的方式是在重試之前首先等待一段時間或者回退,從而避免造成活鎖問題。CAS的主要缺點就是,它將使調用者處理競爭問題,而在鎖中能自動處理競爭問題。雖然java語言的鎖定語句比較簡潔,但JVM和操作在管理鎖時需要完成的工作卻並不簡單。在實現鎖定時需要遍歷JVM中一條非常復雜的代碼路徑,並可能導致操作系統級的鎖定、線程掛起以及上下文卻換等動作。在最好的情況下,在鎖定時至少需要一次CAS,因此雖然在使用鎖時沒有用到CAS,但實際上也無法節約任何執行開銷。另外,在程序內部執行CAS不需要執行JVM代碼、系統調用或線程調度操作。在應用級上看起來越長的代碼路徑,如果加上JVM和操作系統中的代碼調用,那麽事實上卻變得更短。

在非阻塞算法中不存在死鎖和其他活躍性問題。

而在基於鎖的算法中,如果一個線程在休眠或自旋的同時持有一個鎖,那麽其他線程都無法執行下去,而非阻塞算法不會受到單個線程失敗的影響。

鎖的劣勢

許多JVM都對非競爭鎖獲取和釋放操作進行了極大的優化,但如果有多個線程同時請求鎖,那麽JVM就需要借助操作系統地功能。如果出現了這種情況,那麽一些線程將被掛起並且在稍後恢復運行。當線程恢復執行時,必須等待其他線程執行完它們的時間片以後,才能被調度執行。在掛起和恢復線程等過程中存在著很大的開銷,並且通常存在著較大時間的中斷。如果在基於鎖的類中包含細粒度的操作(例如同步器類,在其大多數方法中只包含了少量操作),那麽當在鎖上存在著激烈的競爭時,調度開銷與工作開銷的比值會非常高。

另外,當一個線程正在等待鎖時,它不能做任何其他事情。如果一個線程在持有鎖的情況下被延遲執行,那麽所有需要這個鎖的線程都無法執行下去。如果被阻塞線程的優先級高,而持有鎖的線程優先級低,那麽將是一個嚴重的問題。

比較並交換CAS

CAS包含了3個操作數---需要讀寫的內存位置V,進行比較的值A和擬寫入的新值B。當且僅當V的值等於A時,CAS才會通過原子的方式用新值B來更新V的值,否則不會執行任何操作。無論位置V的值是否等於A,都將返回V原有的值。

CAS的含義:我認為V的值應該為A,如果是,那麽將V的值更新為B,否則不修改並告訴V的值實際為多少。CAS是一種樂觀的態度,它希望能成功地執行更新操作,並且如果有另一個線程在最近一次檢查後更新了該變量,那麽CAS能檢測到這個錯誤。

/**
 * 當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其他線程都將失敗。
 * 然而,失敗的線程並不會被掛起,而是被告知在這次競爭中失敗,並可以再次嘗試。
 * 由於一個線程在競爭CAS時不會阻塞,因此它可以決定是否重新嘗試,或者執行一些恢復操作,也或者不執行任何操作。
 */
public class SimulatedCAS { 
    private int value;
    
    public synchronized int get(){
        return value;
    }
    
    public synchronized int compareAndSwap(int expectedValue,int newValue){
        int oldValue = value;
        if(oldValue == expectedValue){
            value = newValue;
        }
        return oldValue;
    }
    
    public synchronized boolean compareAndSet(int expectedValue,int newValue){
        return (expectedValue == compareAndSwap(expectedValue,newValue));
    }
}

CAS的典型使用模式是:首先從V中讀取值A,並根據A計算新值B,然後再通過CAS以原子方式將V中的值由A變成B。由於CAS能檢測到來自其他線程的幹擾,因此即使不使用鎖也能夠實現原子的讀--改--寫操作

非阻塞的計算器

/**
 * 通常,反復重試是一種合理的策略,但在一些競爭很激烈的情況下,更好的方式是在重試之前首先等待一段時間或者回退,從而避免造成活鎖問題。
 *  
 * 雖然java語言的鎖定語句比較簡潔,但JVM和操作在管理鎖時需要完成的工作卻並不簡單。
 * 在實現鎖定時需要遍歷JVM中一條非常復雜的代碼路徑,並可能導致操作系統級的鎖定、線程掛起以及上下文卻換等動作。
 * 在最好的情況下,在鎖定時至少需要一次CAS,因此雖然在使用鎖時沒有用到CAS,但實際上也無法節約任何執行開銷。
 * 另外,在程序內部執行CAS不需要執行JVM代碼、系統調用或線程調度操作。
 * 在應用級上看起來越長的代碼路徑,如果加上JVM和操作系統中的代碼調用,那麽事實上卻變得更短。
 * CAS的主要缺點是,它要求調用者處理競爭問題,而在鎖中能自動處理競爭問題
 */
public class CasCounter {
    private SimulatedCAS value;
    
    public int getValue(){
        return value.get();
    }
    
    public int increment(){
        int v;
        do{
            v = value.get();
        }while(v != value.compareAndSwap(v, v + 1));
        return v + 1;
    }
}

JVM對CAS的支持

Java5.0中引入了底層的支持,在int,long和對象引用等類型上都公開了CAS操作,並且JVM把它們編譯為底層硬件提供的最有效方法。在原子變量類中,使用了這些底層的JVM支持為數字類型和引用類型提供一種高效的CAS操作,而在java.util.concurrent中的大多數類在實現時都直接或間接地使用了這些原子變量類。

  • 示例:非阻塞的棧
/**
 * 棧是由Node元素構成的一個鏈表,根節點為棧頂yop,每個元素中都包含了一個值以及指向下一個元素的鏈接。
 * push方法創建一個新的節點,該節點的next域指向當前的棧頂,然後使用CAS把這個新節點放入棧頂。
 * 
 * @param <E>
 */
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;
        }
    }
}
  • 示例:非阻塞鏈表

鏈表隊列比棧復雜,它必須支持對頭節點和尾節點的快速訪問。它需要單獨維護頭指針和尾指針。

對於尾部的插入,有兩個點需要更新:將當前尾節點的next指向要插入的節點,和將尾節點更新為新插入的節點。這兩個更新操作需要不同的CAS操作,不好通過原子變量來實現

需要使用一些策略:

策略一是,即使在一個包含多個步驟的更新操作中,也要確保數據結構總是處於抑制的狀態。這樣,線程B到達時,如果發現A正在執行更新,那麽線程B就可以知道有一個操作已部分完成,並且不能立即執行自己的更新操作。然後B可以等待並直到A完成更新。雖然能使不同的線程輪流訪問數據結構,並且不會造成破壞,但如果有一個線程在更新操作中失敗了,那麽其他的線程都無法再方位隊列。

策略二是,如果B到達時發現A正在修改數據結構,那麽在數據結構中應該有足夠多的信息,使得B能完成A的更新操作。如果B幫助A完成了更新操作,那麽B可以執行自己的操作,而不用等待A的操作完成。當A恢復後再試圖完成其操作時,會發現B已經替它完成了。

java多線程11.非阻塞同步機制