Java鎖之ReentrantLock(二)

1、前言

上一篇《Java鎖之ReentrantLock(一)》已經介紹了ReentrantLock的基本源碼,分析了ReentrantLock的公平鎖和非公平鎖機制,最終分析ReentrantLock仍是依託於AbstractQueuedSynchronizer同步隊列器(如下簡稱同步器)實現,因此本篇開始分析同步器內部的代碼實現,考慮到代碼結構比較長,因此分析源碼時會精簡部分不重要的代碼,可是最終仍是會以不影響代碼邏輯的狀況下進行精簡。node

2、同步器分析

  • 同步器主要屬性

 

 

根據上圖源碼咱們能夠知道,AbstractQueuedSynchronizer內部構建了一個Node節點對象,同時構造了一個具備volatile屬性頭節點與尾部節點,保證了多線程之間的可見性,同時最重要的是定義了一個int類型變量state,經過上一篇文章分析,咱們知道了ReenTrantLock是否獲取到鎖的判斷就是state是否大於0,等於0表示鎖空閒,大於0,表示鎖已經被獲取。接下來咱們重點分析下Node節點內部構造以及同步器的實現原理,Node源碼以下:多線程

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;
        //等待條件狀態
        static final int CONDITION = -2;
       //傳播狀態
        static final int PROPAGATE = -3;

       //等待狀態
        volatile int waitStatus;

       //上一個節點
        volatile Node prev;

      //下一個節點
        volatile Node next;

      //獲取同步狀態的線程
        volatile Thread thread;

        //等待隊列中的後繼節點,若是當前節點是共享的,那麼這個nextWaiter=SHARED
        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() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
複製代碼

這裏須要重點說明的屬性是waitStatus,該狀態就是包括節點內部聲明的幾個常量,以下:併發

常量名 功能
CANCELLED 值爲1,當前節點進入取消狀態,緣由是因爲被中斷或者是等待超時而進入取消狀態,須要說明的是,節點線程進入取消狀態後,狀態不會再改變,也就不會再阻塞獲取鎖
SIGNAL 值爲-1,後繼節點的線程處於等待狀態,而當前節點的線程若是釋放了同步狀態或者被取消,將會通知後繼節點,使得後繼節點的線程得以運行
CONDITION 值爲-2,節點在等待隊列中,節點線程等待在Condition上,當其餘線程對Condition調用了signal()方法後,該節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態的競爭中
PROPAGATE 值爲-3,表示下一次共享式同步狀態獲取會無條件的傳播下去,好比若是頭節點獲取到共享式同步狀態,判斷狀態是PROPAGATE,會繼續調用doReleaseShared,使得後繼節點繼續獲取鎖
INITIAL 值爲 0,表示初始狀態(這個應該是老版本中的代碼中存在,目前查看jdk1.8已經沒有顯示聲明INITIAL狀態,由於初始化時候,int變量默認就是0)

分析同步器的屬性,咱們能夠大概畫出構造器的隊列示意圖,以下:工具

 

 

首先同步器聲明瞭頭節點和尾部節點,head節點指向一個node節點表示該節點是隊列的頭部節點,tail節點指向一個node節點表示該節點是尾部節點,同時,每一個節點都有pre和next屬性,指向node節點,而後如圖所示構建成一個FIFO雙向鏈表式隊列。下面咱們查看下同步器經常使用主要方法性能

  • 同步器主要方法列表

方法名稱 功能
compareAndSetState(int expect, int update) CAS進行設置同步狀態
enq(final Node node) 循環入等待隊列,直到入隊成功爲止
addWaiter(Node mode) 以當前線程建立一個尾部節點,並加入到尾部
unparkSuccessor(Node node) 喚醒節點的後繼節點
doReleaseShared() 釋放共享模式下的同步狀態
setHeadAndPropagate(Node node, int propagate) 設置頭節點,並繼續傳播同步許可
release(int arg) 獨佔式釋放同步狀態
acquireShared(int arg) 共享式釋放同步狀態
hasQueuedPredecessors() 判斷是否有比當前線程等待更久的線程(用於公平鎖)
  • 同步器加鎖

以上是同步器主要的方法,咱們接下來會對上述部分方法進行重點分析,要了解同步器如何完成加鎖,等待獲取鎖,釋放鎖的功能,咱們先回顧上一篇文章分析ReentranLock的Lock()方法,實現源碼以下:ui

/**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
複製代碼

咱們發現重點是acquire(1)方法,該方法是父類也就是同步器提供的,源碼以下:this

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
複製代碼

源碼其實能夠拆分爲三部分:spa

  • tryAcquire(arg) 嘗試獲取鎖
  • addWaiter(Node.EXCLUSIVE), arg) 以當前線程構建成節點添加到隊列尾部
  • acquireQueued(final Node node, int arg)讓節點以死循環去獲取同步狀態,獲取成功就退出循環

其實解析爲三部分就很清楚這個方法的做用了,首先嚐試獲取鎖,獲取不到就把本身添加到尾部,而後在隊列中死循環去獲取鎖,最重要的部分就是acquireQueued(final Node node, int arg),源碼以下:操作系統

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
               //獲取節點的前任節點
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) { //若是前任節點是頭節點,
                //而且當前節點獲取到了鎖,
                //也就是說隊列頭節點釋放了鎖,同時把當前節點設置爲頭節點
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //判斷當前節點是否應該被阻塞,那麼就把當前線程阻塞掛起,防止無謂的死循環
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
複製代碼
//該方法主要靠前驅節點判斷當前線程是否應該被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 若是前任節點的狀態等於SIGNAL,
             * 說明前任節點獲取到了同步狀態,當前節點應該被阻塞,返回true
             */
            return true;
        if (ws > 0) {
            /*
             * 前任節點被取消
             */
            do {//循環查找取消節點的前任節點,
            //直到找到不是取消狀態的節點,而後剔除是取消狀態的節點,
            //關聯前任節點的下一個節點爲當前節點
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * CAS設置前任節點等待狀態爲SIGNAL,
             * 設置成功表示當前節點應該被阻塞,下一次循環調用就會
            *  return  true
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
複製代碼
//把當前線程掛起,從而阻塞住線程的調用棧,
//同時返回當前線程的中斷狀態。
//其內部則是調用LockSupport工具類的park()方法來阻塞該方法。
   private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//阻塞線程
        return Thread.interrupted();
    }
複製代碼

以上就是lock.lock()的加鎖過程,咱們總結分析下:.net

  • 首先,直接嘗試獲取鎖,獲取成功直接結束。
  • 若是獲取鎖失敗,就把當前線程構造一個尾部節點,CAS方式加入到隊列的尾部
  • 在隊列中,死循環式的判斷前任節點是不是頭節點,若是是頭節點就嘗試獲取鎖,若是不是就把本身掛起,等待前任節點喚醒本身,這樣能夠避免多個線程死循環帶來的性能消耗。
  • 同步器解鎖

    lock.unlock()釋放鎖的過程,分析源碼,老規矩,上源碼:
//釋放鎖
   public void unlock() {
        sync.release(1);
    }
    
     public final boolean release(int arg) {
        //嘗試釋放鎖
        if (tryRelease(arg)) {
            Node h = head;
            //若是頭節點不爲空,而且不是初始狀態,也就是不在隊列中了
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//喚醒後繼節點
            return true;
        }
        return false;
    }
    //該方法和以前分析的代碼相似,主要是設置Sate狀態
     protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);//設置同步狀態佔有線程爲null
            }
            setState(c);
            return free;
        }
        
    private void unparkSuccessor(Node node) {
       
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // 取到當前節點的下一個節點,若是節點不爲空就喚醒阻塞的線程
        //若是節點爲空,或者節點是取消狀態,那麼就循環從尾部節點找到
        //當前節點的下一個節點喚醒
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//喚醒阻塞的線程
    }    
複製代碼

以上就是對lock.unlock()分析,一樣咱們總結分析下

  • 首先既然加鎖成功與否判斷是根據State不爲0來判斷,因此,釋放鎖就會把State設置爲0,同時設置鎖的全部者線程爲null
  • 鎖釋放成功了,接着就會喚醒在隊列的後繼節點,經過調用LockSupport.unpark(s.thread)來喚醒線程的,LockSupport主要依託於sun.misc.Unsafe類來實現的,該類提供了操做系統硬件級別的方法,不在本文討論中。

3、尾言

  • 本次主要分析了AQS同步器的加鎖和解鎖的實現,其實jdk不少同步工具類都是依賴於AQS同步器實現的,瞭解了AQS同步器的原理後,對理解其餘併發工具的原理也頗有幫助,好比CountDownLatchSemaphore,CyclicBarrier(依賴ReentrantLock)等。下一篇《Java鎖之ReentrantReadWriteLock》繼續細化分析,分析讀鎖和寫鎖,分析ReentrantLock是如何實現讀鎖的重複獲取,鎖降級等功能的。
相關文章
相關標籤/搜索