深入理解AQS-2 鎖基礎知識

悲觀鎖和樂觀鎖

synchronized同步方法最主要的問題是線程阻塞和喚醒帶來的性能消耗,阻塞同步是悲觀的併發策略,只要有可能出現競爭,都認爲一定要先加鎖;然而還有一種樂觀的併發策略,直接操作數據,如果沒有發現其他線程同時操作數據則認爲這個操作是成功的,如果其他線程也操作了數據,那麼操作是失敗的,一般採用不斷重試的手段(自旋),直到成功爲止。樂觀策略適用於併發程度不高且臨界區較小的場景,優點是不需要阻塞線程,屬於非阻塞同步手段,性能更高。

CAS

樂觀鎖併發策略主要有兩個重點階段,一個是對數據進行操作,另一個是檢測是否發生衝突(即是否存在同時操作數據的其他線程),這裏操作數據和衝突檢測需要具備原子性,即操作數據和衝突檢查必須同時成功或者同時失敗,這個原子性通過CAS指令來實現,目前絕大多數的CPU都支持CAS指令。

CAS指令需要三個參數:分別是內存地址,期望的舊值,新值。

自旋鎖

自旋鎖是樂觀鎖的一種實現,當線程曲獲取一個鎖時,如果發現該鎖被其他線程佔用,那麼進入一個無意義的循環不斷嘗試加鎖,直到成功獲取鎖。自旋鎖適用於臨界區比較小的場景,如果鎖被持有的時間過長,那麼自旋本身會長時間的白白浪費CPU資源。

public class SpinLock {
    private final AtomicReference<Thread> owner = new AtomicReference<Thread>();
    public  void lock(){
        while (!owner.compareAndSet(null, Thread.currentThread())){
            System.out.println(Thread.currentThread().getName() + " loop");
        }
        System.out.println(Thread.currentThread().getName() + " lock");
    }
    public void unlock(){
        owner.compareAndSet(Thread.currentThread(), null);
        System.out.println(Thread.currentThread().getName() + " unlcok");
    }
}

上述代碼中owner變量保存了持鎖線程,這裏有兩個缺點,第一個是沒有保證公平性,另一個是由於多個線程同時操作同一個共享變量owner,而每個CPU都會緩存該變量,任意一個線程加鎖和解鎖後,其他所有CPU中的owner緩存都立刻失效,而線程自旋過程中又都會使用(讀)該變量,所以各個CPU都需要重新讀內存(CPU的緩存一致性原理),因此自旋鎖會頻繁的進行緩存一致性同步操作,每次加鎖和解鎖都會帶來一次,這導致繁重的系統總線流量和內存操作,會降低性能。

公平自旋鎖

爲了解決公平性問題,可以讓鎖維護一個編號來表示下次該獲取鎖的線程,每個線程在申請鎖時首先被分配一個編號,然後始終自旋直到輪到自己然後嘗試加鎖。雖然解決了公平性問題,但是依然存在緩存同步導致性能下降的問題。

public class FairSpinLock {
    private final AtomicInteger nextNo = new AtomicInteger();
    private final AtomicInteger threadNo = new AtomicInteger();
    public  int lock(){
        int myNo = threadNo.getAndIncrement();
        while (nextNo.get() != myNo){
        }
        System.out.println(Thread.currentThread().getName() + " lock");
        return myNo;
    }
    public void unlock(int threadNo){
        int next = threadNo + 1;
        nextNo.compareAndSet(threadNo, next);
        System.out.println(Thread.currentThread().getName() + " unlcok");
    }
}

MCS自旋鎖

自旋鎖之所以頻繁的發生緩存失效的問題,是因爲所有線程加鎖和解鎖都會操作同一個變量,因此如果是不同的變量,或者說多個線程操作不同變量,那麼可避免高頻率的緩存同步操作,這就是MCS的實現思路。

MCS基於鏈表實現,每個申請鎖的線程都對應鏈表上的一個節點,這些線程一直輪詢自身節點來確定自己是否獲得了鎖。獲得鎖的線程在釋放鎖的時候,負責通知後繼節點已獲取鎖,即更新後繼節點的運行狀態,這會導致其他CPU中該變量的緩存失效,但並不是所有線程都會使用這個後繼節點,所以不會發生所有CPU同時進行緩存一致性同步操作,而且僅在線程通知後繼線程時發生一次緩存失效,這樣緩存同步操作就減少很多,降低了系統總線和內存的開銷。

不支持重入,可加一個ThreadLocal變量記錄重入次數來實現。

public class McsLock {
    /**
     *   隊尾
     */
    private volatile Node tail;

    /**
     * 隊尾原子操作
     */
    private static final AtomicReferenceFieldUpdater<McsLock, Node> TAIL_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(McsLock.class, Node.class, "tail");
    /**
     * 線程到節點的映射
     */
    private ThreadLocal<Node>  currentThreadNode = new ThreadLocal<>();

    /**
     * 加鎖
     */
    public void lock(){
        Node cNode = currentThreadNode.get();
        if (cNode == null){
            cNode =  new Node();
            currentThreadNode.set(cNode);
        }
        //原子地入隊並返回原隊尾
        Node predecessor = TAIL_UPDATER.getAndSet(this, cNode);  //step 1
        if (predecessor != null){
            //需要排隊
            predecessor.setNext(cNode);   //step 2
            while (cNode.isWaiting){
                //自旋等待前置線程更新自己的狀態   //step
            }
        }else{
            //無需排隊,自己更新自己狀態
            cNode.setWaiting(false);
        }
        System.out.println(Thread.currentThread().getName() + " lock");
        return;
    }

    /**
     * 解鎖
     */
    public void unlock(){
        // 獲取當前線程對應的節點
        Node cNode = currentThreadNode.get();
        if (cNode == null || cNode.isWaiting){
            throw new RuntimeException(cNode + " not lock");
        }
        if (cNode.getNext() == null && !TAIL_UPDATER.compareAndSet(this, cNode, null)){
            // 沒有後繼節點的情況,將tail置空
            // 如果CAS操作失敗了表示突然有節點排在自己後面了,可能還不知道是誰,下面是等待後繼節點入隊
            // 這裏之所以要忙等是因爲上述的lock操作中step 1執行完後,step 2可能還沒執行完
            while (cNode.getNext() == null){
                //step 5
            }
        }
        if (cNode.getNext() != null){
            // 通知後繼節點獲取鎖
            cNode.getNext().setWaiting(false);
            //help GC
            cNode.setNext(null);
        }
        System.out.println(Thread.currentThread().getName() + " unlock");
    }

    /**
     * 隊列節點類
     */
    private static class Node {
        //默認是等待狀態
        private volatile boolean isWaiting = true;
        //後繼節點
        private volatile Node next;

        public boolean isWaiting() {
            return isWaiting;
        }

        public void setWaiting(boolean waiting) {
            isWaiting = waiting;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }

CLH自旋鎖

CLH鎖和MCS鎖的原理大致相同,都是各個線程各自關注各自的變量,來避免多線程同時操作同一個變量,從而減少緩存同步;不同點在於MCS自旋輪詢的是當前節點的屬性,而CLH輪詢的是前驅節點的屬性,來判斷前一個線程是否釋放了鎖。

public class ClhLock {
    public static class Node {
        //是否結束
        private volatile boolean isOver = false;
    }
    private volatile Node tail;

    private static AtomicReferenceFieldUpdater<ClhLock, Node> TAIL_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(ClhLock.class, Node.class, "tail");

    public void lock(Node cThread){
        Node predesser = TAIL_UPDATER.getAndSet(this, cThread);
        if (null != predesser){
            while (predesser.isOver){}
        }
        System.out.println(Thread.currentThread().getName() + " lock");
    }

    public void unlock(Node cThread){
        if (!TAIL_UPDATER.compareAndSet(this, cThread, null)){
            //存在等待線程
            cThread.isOver = true;
        }
        System.out.println(Thread.currentThread().getName() + " unlock");
    }

從代碼可知,CLH比MCS要簡潔很多;CLH是在前驅節點的屬性上自旋,其等待隊列是隱式的,節點並不實際持有前驅或者後繼,通過每個節點都輪詢前驅形成邏輯鏈表,而MCS的隊列是物理存在的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章