j.u.c系列(03)---之AQS:AQS簡介

寫在前面  

  Java的內置鎖一直都是備受爭議的,在JDK 1.6以前,synchronized這個重量級鎖其性能一直都是較爲低下,雖然在1.6後,進行大量的鎖優化策略,可是與Lock相比synchronized仍是存在一些缺陷的:雖然synchronized提供了便捷性的隱式獲取鎖釋放鎖機制(基於JVM機制),可是它卻缺乏了獲取鎖與釋放鎖的可操做性,可中斷、超時獲取鎖,且它爲獨佔式在高併發場景下性能大打折扣。node

  在介紹Lock以前,咱們須要先熟悉一個很是重要的組件,掌握了該組件JUC包下面不少問題都不在是問題了。該組件就是AQS。安全

AQS簡介

  AQS,AbstractQueuedSynchronizer,即隊列同步器。它是構建鎖或者其餘同步組件的基礎框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC併發包的做者(Doug Lea)指望它可以成爲實現大部分同步需求的基礎。它是JUC併發包中的核心基礎組件。數據結構

  AQS解決了子啊實現同步器時涉及當的大量細節問題,例如獲取同步狀態、FIFO同步隊列。基於AQS來構建同步器能夠帶來不少好處。它不只可以極大地減小實現工做,並且也沒必要處理在多個位置上發生的競爭問題。併發

  在基於AQS構建的同步器中,只能在一個時刻發生阻塞,從而下降上下文切換的開銷,提升了吞吐量。同時在設計AQS時充分考慮了可伸縮行,所以J.U.C中全部基於AQS構建的同步器都可以得到這個優點。框架

  AQS的主要使用方式是繼承,子類經過繼承同步器並實現它的抽象方法來管理同步狀態。高併發

  AQS使用一個int類型的成員變量state來表示同步狀態,當state>0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。它提供了三個方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))來對同步狀態state進行操做,固然AQS能夠確保對state的操做是安全的。性能

  AQS經過內置的FIFO同步隊列來完成資源獲取線程的排隊工做,若是當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構形成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。優化

 

AQS方法

  • getState():返回同步狀態的當前值;
  • setState(int newState):設置當前同步狀態;
  • compareAndSetState(int expect, int update):使用CAS設置當前狀態,該方法可以保證狀態設置的原子性;
  • tryAcquire(int arg):獨佔式獲取同步狀態,獲取同步狀態成功後,其餘線程須要等待該線程釋放同步狀態才能獲取同步狀態;
  • tryRelease(int arg):獨佔式釋放同步狀態;
  • tryAcquireShared(int arg):共享式獲取同步狀態,返回值大於等於0則表示獲取成功,不然獲取失敗;
  • tryReleaseShared(int arg):共享式釋放同步狀態;
  • isHeldExclusively():當前同步器是否在獨佔式模式下被線程佔用,通常該方法表示是否被當前線程所獨佔;
  • acquire(int arg):獨佔式獲取同步狀態,若是當前線程獲取同步狀態成功,則由該方法返回,不然,將會進入同步隊列等待,該方法將會調用可重寫的tryAcquire(int arg)方法;
  • acquireInterruptibly(int arg):與acquire(int arg)相同,可是該方法響應中斷,當前線程爲獲取到同步狀態而進入到同步隊列中,若是當前線程被中斷,則該方法會拋出InterruptedException異常並返回;
  • tryAcquireNanos(int arg,long nanos):超時獲取同步狀態,若是當前線程在nanos時間內沒有獲取到同步狀態,那麼將會返回false,已經獲取則返回true;
  • acquireShared(int arg):共享式獲取同步狀態,若是當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔式的主要區別是在同一時刻能夠有多個線程獲取到同步狀態;
  • acquireSharedInterruptibly(int arg):共享式獲取同步狀態,響應中斷;
  • tryAcquireSharedNanos(int arg, long nanosTimeout):共享式獲取同步狀態,增長超時限制;
  • release(int arg):獨佔式釋放同步狀態,該方法會在釋放同步狀態以後,將同步隊列中第一個節點包含的線程喚醒;
  • releaseShared(int arg):共享式釋放同步狀態;

 

CLH同步隊列

  CLH同步隊列是一個FIFO雙向隊列,AQS依賴它來完成同步狀態的管理,當前線程若是獲取同步狀態失敗時,AQS則會將當前線程已經等待狀態等信息構形成一個節點(Node)並將其加入到CLH同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點喚醒(公平鎖),使其再次嘗試獲取同步狀態。ui

  在CLH同步隊列中,一個節點表示一個線程,它保存着線程的引用(thread)、狀態(waitStatus)、前驅節點(prev)、後繼節點(next),其定義以下:this

static final class Node {
    /** 共享 */
    static final Node SHARED = new Node();
    /** 獨佔 */
    static final Node EXCLUSIVE = null;
    /**
     * 由於超時或者中斷,節點會被設置爲取消狀態,被取消的節點時不會參與到競爭中的,他會一直保持取消狀態不會轉變爲其餘狀態;
     */
    static final int CANCELLED =  1;
    /**
     * 後繼節點的線程處於等待狀態,而當前節點的線程若是釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行
     */
    static final int SIGNAL    = -1;
    /**
     * 節點在等待隊列中,節點線程等待在Condition上,當其餘線程對Condition調用了signal()後,改節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態的獲取中
     */
    static final int CONDITION = -2;
    /**
     * 表示下一次共享式同步狀態獲取將會無條件地傳播下去
     */
    static final int PROPAGATE = -3;
    /** 等待狀態 */
    volatile int waitStatus;
    /** 前驅節點 */
    volatile Node prev;
    /** 後繼節點 */
    volatile Node next;
    /** 獲取同步狀態的線程 */
    volatile Thread thread;
    Node nextWaiter;
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    Node() {
    }
    Node(Thread thread, Node mode) {
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) {
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

CLH同步隊列結構圖以下:

入列

  學了數據結構的咱們,CLH隊列入列是再簡單不過了,無非就是tail指向新節點、新節點的prev指向當前最後的節點,當前最後一個節點的next指向當前節點。代碼咱們能夠看看addWaiter(Node node)方法:

    private Node addWaiter(Node mode) {
        //新建Node
        Node node = new Node(Thread.currentThread(), mode);
        //快速嘗試添加尾節點
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //CAS設置尾節點
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //屢次嘗試
        enq(node);
        return node;
    }

  addWaiter(Node node)先經過快速嘗試設置尾節點,若是失敗,則調用enq(Node node)方法設置尾節點

  private Node enq(final Node node) {
        //屢次嘗試,直到成功爲止
        for (;;) {
            Node t = tail;
            //tail不存在,設置爲首節點
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //設置爲尾節點
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

  在上面代碼中,兩個方法都是經過一個CAS方法compareAndSetTail(Node expect, Node update)來設置尾節點,該方法能夠確保節點是線程安全添加的。在enq(Node node)方法中,AQS經過「死循環」的方式來保證節點能夠正確添加,只有成功添加後,當前線程纔會從該方法返回,不然會一直執行下去。

過程圖以下:

 

出列

  CLH同步隊列遵循FIFO,首節點的線程釋放同步狀態後,將會喚醒它的後繼節點(next),然後繼節點將會在獲取同步狀態成功時將本身設置爲首節點,這個過程很是簡單,head執行該節點並斷開原首節點的next和當前節點的prev便可,注意在這個過程是不須要使用CAS來保證的,由於只有一個線程可以成功獲取到同步狀態。過程圖以下:

相關文章
相關標籤/搜索