面試準備之ReentrantLock之源碼分析

目錄

1.AQS

2.ReentrantLock經典題

3.ReentrantLock源碼分析

3.1ReentrantLock結構圖​

3.2ReentrantLock的構造方法

3.3獲取鎖lock()方法

3.4 釋放鎖

4.總結ReentrantLock的基本流程:​

5.ReentrantLock公平鎖

6.ReentrantLock和Synchronized的共同點和不同點


ReentrantLock作爲Java中除了synchronized之外用的最多的鎖。ReentrantLock是利用AQS框架實現的樂觀鎖,在介紹ReentrantLock之前先看一下AQS也就是AbstractQueuedSynchronizer。

1.AQS

AQS是一個同步框架用來控制多線程訪問共享資源。使得多線程有順序的去訪問共享資源。AQS中有兩個很重要的元素

A.volatitle修飾狀態值state,線程通過CAS操作來改變state的值來作爲這個線程是否獲取到鎖的依據。一般都是默認state等於0的時候,這個時候鎖是沒有被其他線程獲取的,通過CAS操作改變了state的值,如果改變成功說明獲取到了鎖,如果改變不成功,說明沒有獲取到鎖

B.同步FIFO隊列,當線程沒有成功獲取鎖的時候,會將這個線程加入到隊列中,然後當線程釋放鎖後,會從隊列中喚醒一個線程重新佔有鎖。這個隊列是一個雙向隊列,但這個隊列不是一個真實聲明的隊列,它是通過一個個的Node組成的,每個Node都是對沒有獲取到鎖的線程的封裝,而且Node是有指向它前面和後面Node的指針,這樣多個Node組合在一起就形成了一個雙向隊列,這個Node是AbstractQueuedSynchronizer裏面的內部類。隊列的結構圖
在這裏插入圖片描述
Node對象的代碼

static final class Node {
        //標記表示節點正在共享模式中等待
        static final Node SHARED = new Node();
        //標記表示節點正在獨佔模式下等待
        static final Node EXCLUSIVE = null;
        //waitStatus值表示線程已取消
        static final int CANCELLED =  1;
        //waitStatus值表示後繼者的線程需要被喚醒
        static final int SIGNAL    = -1;
        //waitStatus值表示線程正在等待條件
        static final int CONDITION = -2;
        //waitStatus值表示下一個acquireShared應無條件傳播
        static final int PROPAGATE = -3;
        
        volatile int waitStatus;
       // 當前節點的前驅節點
        volatile Node prev;
	   // 當前節點的後續節點
        volatile Node next;
      // 當前節點指向的線程
        volatile Thread thread;

      // nextWaiter是“區別當前CLH隊列是 ‘獨佔鎖’隊列 還是 ‘共享鎖’隊列 的標記”
     // 若nextWaiter=SHARED,則CLH隊列是“共享鎖”隊列;
     // 若nextWaiter=EXCLUSIVE,(即nextWaiter=null),則CLH隊列是“獨佔鎖”隊列。
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         * 返回前驅節點
         */
        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
        }

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

用一句話總結AQS框架就是線程通過CAS操作來改變state的值,如果成功說明這個線程拿到鎖了,如果不成功就放入隊列中掛起,等待它的前一個線程來喚醒它重新對state進行CAS操作來獲取鎖

2.ReentrantLock經典題

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author wangbiao
 * @Date 2019-11-17 16:59
 * @Decripition TODO
 **/
public class ReetrantLockTest {
    private Lock lock = new ReentrantLock();
    private Condition conditionA = lock.newCondition();
    private Condition conditionB = lock.newCondition();
    private Condition conditionC = lock.newCondition();

    public void printA(){

        try{
            lock.lock();
            for(int i=0;i<10;i++){

                    System.out.println(Thread.currentThread().getName()+"打印A");
                    conditionB.signal();
                    conditionA.await();

            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }


    }

    public void printB(){
        try{
            try{
                lock.lock();
                for(int i=0;i<10;i++){

                        System.out.println(Thread.currentThread().getName()+"打印B");
                        conditionC.signal();
                        conditionB.await();

                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }


    public void printC(){
        try{
            try{
                lock.lock();
                for(int i=0;i<10;i++){

                    System.out.println(Thread.currentThread().getName()+"打印C");
                    conditionA.signal();
                    conditionC.await();

                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception{
        ReetrantLockTest test = new ReetrantLockTest();
        Thread threadA = new Thread(){
            public void run(){
                test.printA();
            }
        };
        Thread threadB = new Thread(){
            public void run(){
                test.printB();
            }
        };
        Thread threadC = new Thread(){
            public void run(){
                test.printC();
            }
        };
        threadA.start();
        Thread.sleep(100);
        threadB.start();
        Thread.sleep(100);
        threadC.start();

    }


}

最後輸出:

Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C
Thread-0打印A
Thread-1打印B
Thread-2打印C

3.ReentrantLock源碼分析

3.1ReentrantLock結構圖
在這裏插入圖片描述
 

ReentrantLock實現了Lock接口,並且內部有三個內部類Sync,NonFairSyn和FairSync,這裏運用了模板模式,在AbstractQueuedSynchronizer裏面實現了加鎖和釋放鎖的抽象操作,就是大體的操作流程,但是具體實現例如tryAcquire,tryRelease這些方法就要靠具體的實現類來實現了,我覺得還用到了適配器模式,通過實現Lock接口,但在實現方法裏面引用了Sync的具體方法。


3.2ReentrantLock的構造方法

可以發現ReentrantLock默認的是非公平鎖,如果在申明ReentrantLock的時候,向構造器裏面傳入一個true,那ReentrantLock就是一個公平鎖

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
   // 同步器的引用
    private final Sync sync;
   //非公平鎖的構造函數
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    // 公平鎖的構造函數
	public ReentrantLock(boolean fair) {
	// 三目運算符,如果爲true 則爲公平鎖,反之爲非公平鎖
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

3.3獲取鎖lock()方法

public void lock() {
        sync.lock();
    }

可以看到lock方法實際調用的是NonFairSync裏面的lock方法,先直接利用CAS操作嘗試去改變state的值,如果成功,就說明獲取到了鎖,如果沒有成功再調用acquire,公平鎖的lock方法和這裏不太一樣,公平鎖的lock不會直接去嘗試改變state的值,而是直接調用acquire方法。
NonFairSync.lock方法

final void lock() {
        //嘗試直接改變state的值,如果成功說明獲取到鎖
        if (compareAndSetState(0, 1))
            //將鎖的獨佔線程設置爲當前線程
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //如果直接嘗試改值失敗,就進行下面的操作
            acquire(1);
    }

AbstractQueuedSynchronizer.acuqre方法

 /**
     * tryAcquire(arg)方法
     * 1.嘗試通過CAS操作改變state值來拿到鎖
     * addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE)
     * 2.操作失敗就將線程封裝成Node並放入隊尾
     *  acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg)
     * 3。放入到隊尾後判斷上一個節點是否是隊頭,如果是的,就嘗試獲取鎖,獲取失敗就掛起
     * 如果不是隊頭就掛起
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(AbstractQueuedSynchronizer.Node.EXCLUSIVE), arg))
            //如果沒有獲取到鎖,但是有中斷操作,就自己中斷
            selfInterrupt();
    }

到回到NonFairSync的tryAcquire方法,先判斷state是否等於0,等於0說明當前鎖沒有被佔用,利用CAS操作更改state的值,要是更改成功說明獲取鎖成功,如果state不等於0,就判斷佔有鎖的線程是不是當前線程,要是是的,就對state進行加一操作。

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
​
/**
     * 先嚐試利用CAS操作更改state的值,如果更改成功,說明拿到鎖了,
     * 如果不成功,說明這個鎖已經有線程佔有了,那就判斷佔有這個鎖的線程是否是當前線程
     * 如果是的,state值進行加一操作,這裏也體現了ReentrantLock是可重入鎖
     * @param acquires
     * @return
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        //獲取當前鎖的狀態值,如果是等於0,說明沒有線程佔有這個鎖
        int c = getState();
        if (c == 0) {
            //通過cas改變狀態值,如果更改成功,說明取得了鎖
            if (compareAndSetState(0, acquires)) {
                //將擁有鎖的線程替換成當前線程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //如果擁有鎖的線程是當前線程,則將狀態值加一
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

​

AbstractQueuedSynchronized的addWaiter方法,這個方法就是將當前線程封裝成Node,並快速添加到隊尾,如果快速添加失敗,就通過for的無限循環插入到隊尾

private Node addWaiter(Node mode) {
        //先將當前線程封裝成Node,由於mode是null,所以是獨佔模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 獲取隊尾,通過cas快速替換當前tail節點爲node節點
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果快速插入失敗,則利用無限for循環進行插入操作
        enq(node);
        return node;
    }
/**
     * 一種常見的CAS操作,就是無限for循環,直到CAS操作成功才跳出for循環
     * @param node
     * @return
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //如果隊尾是空的,就先初始化一個節點,並設置爲頭節點,隊尾也指向頭節點
            //然後執行完後再循環一次,隊尾就不會爲空了
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //將node設置爲隊尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

成功將Node插入到隊尾後,判斷這個隊尾的前一個Node是不是隊頭,如果是隊頭的話嘗試獲取鎖,如果它前面的節點不是隊頭的話就掛起,等待被喚醒後再次執行判斷它前面的Node是不是隊頭,如果是隊頭就嘗試獲取鎖,如果獲取成功,就將這個節點設置成頭節點。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 死循環,直到滿足條件退出
            // 首先將當前線程進行阻塞,等待其他線程進行喚醒
            // 其他線程進行喚醒以後,判斷當前是否獲取資源。此時,可能有其他線程的加入,導致獲取失敗
            for (;;) { 
            // 獲取當前節點的前驅節點
                final Node p = node.predecessor();
            // 如果當前節點的前驅節點是head節點,且嘗試獲取鎖成功
                if (p == head && tryAcquire(arg)) {
                // 設置當前的node節點爲head節點
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 判斷當前線程是否應該阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 如果當前線程被中斷,則 parkAndCheckInterrupt()返回爲true。interrupted = true
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
 /**
     * 如果Node的前節點的waitStatus是等於1的,那麼就直接可以確認將Node掛起的
     * 如果waitStatus是大於0的,則代表線程被取消,重新設置Node的前驅節點
     * 找到第一個ws<=0的節點,將這個節點設置爲Node的前驅節點
     * 默認的Node的ws是null的,所以會將ws設置爲SIGNAL,也就是-1
     *
     * @param pred
     * @param node
     * @return
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

將當前線程掛起,掛起後如果被喚醒,就判斷當前線程是否被中斷了

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

總結一下ReentrantLock非公平鎖的獲取:

1.嘗試通過cas操作來改變state的值,如果能改變則獲取到鎖。

2.如果沒能改變state的值,則判斷state的值是否爲0,如果爲0則嘗試用cas操作來改變state值來拿鎖。

3.如果state的值不爲0,則判斷拿到鎖的線程是否是自己,如果是的,則state加一操作,如果獲取鎖的線程不是自己,則將自己封裝成一個node節點,放入到隊尾。

4.放入隊尾後,判斷自己是否是頭節點,如果是頭節點,則嘗試改變state的值獲取鎖,如果不是頭節點,則掛起來,等待前面的節點來喚醒。

3.4 釋放鎖


調用的AbstractQueuedSynchronizer的release方法,

public void unlock() {
        sync.release(1);
    }
/**
     * 如果釋放資源成功,並且頭節點不爲空,並且頭節點的狀態值不等於0,
     * 就喚醒下一個Node節點來獲取鎖
     * @param arg
     * @return
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease方法,鎖的狀態值進行減一操作,如果狀態值等於0,就說明這個鎖被完全釋放

/**
     * 先對鎖的狀態值進行減一操作,如果當前線程不是獲取到鎖的線程,就拋出IllegaMonitorStateException異常
     * 如果狀態值在減一後變成0,那麼說明這個鎖被完全釋放了,將佔有鎖的線程設置成空
     * @param releases
     * @return
     */
    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);
        }
        setState(c);
        return free;
    }

釋放完鎖後就需要喚醒下一個節點來獲取鎖,AbstractQueuedSynchronizer的unparkSuccessor

private void unparkSuccessor(AbstractQueuedSynchronizer.Node node) {
        //把狀態值設爲0
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /**
         * 獲取Node的後驅元素,如果後驅節點是空,或者狀態是取消,
         * 要找到離隊頭最近的狀態是小於等於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);
    }

4.總結ReentrantLock的基本流程:
這裏寫圖片描述

這裏是對於非公平鎖而言的流程
加鎖的過程
A.直接利用CAS更改同步狀態值,如果成功,就將鎖的獨佔線程設置成當前線程,說明獲取到鎖
B.更改狀態值失敗,在tryAcquire方法裏面獲取狀態值,如果狀態值等於0,就利用CAS操作更改狀態值,成功說明獲取到鎖
   如果狀態值不等於0,判斷擁有鎖的線程是不是當前線程,如果是的,就對狀態值加一操作,拿到鎖
C.B過程中沒有拿到鎖,就將當前線程封裝成Node節點,並快速插入到隊尾,如果快速插入失敗的話,要是隊頭爲null的話,先初始化一個Node節點作爲隊尾,再然後將Node節點作爲隊尾插入。
D.插入隊尾完成後,利用無限for循環,先判斷當前Node節點的前驅節點是否是隊頭,如果是的,嘗試獲取鎖,如果獲取成功就直接返回,如果失敗或者前驅節點不是隊頭,判斷自己的前驅節點的狀態是否是SIGNAL,SIGNAL代表可以喚醒後驅節點,如果是的直接返回true,如果不是的,就找到最近的一個狀態是SIGNAL的節點作爲前驅節點返回true,再將自己掛起,等待喚醒。
E.掛起後,如果被喚醒,判斷自己是否被中斷了,如果被中斷就自己嘗試中斷自己。

解鎖的過程
A.更改狀態值也就是減一操作,判斷當前線程是否是持有鎖的線程,如果是的,則判斷釋放後的狀態值是否爲0,如果是的說明鎖被完全釋放,返回true,如果鎖沒有被完全釋放,返回false
B.如果鎖被完全釋放,就喚醒隊頭的後驅節點,喚醒的前提是後驅節點的waitstatus是小於等於0的,如果不是的,找到離隊頭最近的waitstatus是小於等於0的節點進行喚醒,喚醒調用LockSupport.unpark(s.thread);

5.ReentrantLock公平鎖


ReentrantLock默認的是非公平鎖,當申明ReentrantLock實例的時候向構造函數裏面傳參數true的時候,就是公平鎖了,非公平鎖和公平鎖最多的區別是在都調用lock方法的時候:
非公平鎖調用lock方法
首先是直接去更改鎖的狀態值來嘗試獲取鎖,如果獲取失敗了,再進行下面的操作

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

公平鎖加鎖的操作:

final void lock() {
            acquire(1);
        }
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

在FairSync重寫tryAcquire方法的時候,去獲取鎖的前面加了一個hasQueuedPredecessors的判斷,這個判斷是判斷隊列中有沒有比自己排在更前面的節點,也就是頭節點的下一個節點是否存在。

public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

總結一下公平鎖和非公平鎖在獲取鎖的過程中有什麼不同:
非公平鎖加鎖的時候是直接先去利用CAS操作更改鎖的狀態值來判斷有沒有拿到鎖,如果沒有拿到,再去判斷鎖的狀態值是否等於0,如果等於0再去嘗試獲取鎖。
公平鎖加鎖的時候會先去獲取鎖的狀態值是否等於0,如果等於0,還要去判斷當前隊列中是否有排在自己前面的節點,如果沒有才去獲取鎖。這也是在新東方面試的時候沒有回答上來的問題。


6.ReentrantLock和Synchronized的共同點和不同點


共同點:
兩者都是可重入的獨佔鎖,ReentrantLock在默認的情況下是非公平鎖,Synchronized也是非公平鎖
不同點:
1.Synchronized是在字節碼指令進行加鎖操作,ReentrantLock實在代碼層面進行加鎖操作
2.Synchronized鎖的釋放在執行完同步代碼塊或者同步方法的時候就會自動釋放,ReentrantLock加完鎖後得調用unlock方法手動釋放,所以一般將unlock方法在finally裏面來釋放的
3.線程在獲取Synchronized的同步鎖的時候如果被阻塞了,會一直阻塞,不能被中斷,不能幹別的,而ReentrantLock在加鎖的時候選擇tryLock加鎖,可以在沒有獲取到鎖後直接返回,不用阻塞,或者lockInterruptibly方法加鎖的話,可以在阻塞的過程中被中斷
4.如果在加鎖的代碼塊中運行發生異常,Synchronized會釋放鎖,ReentrantLock不會釋放鎖,所以一般將ReentrantLock鎖的釋放放在finally代碼塊裏面。
5.ReentrantLock結合condition使用會比Synchronized和wait,notify結合使用的話更加靈活,ReentrantLock可以結合condition來喚醒和鎖定特定的線程。

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