java多線程併發重入鎖ReentrantLock源碼詳細分析

synchronized同步代碼塊

一個線程訪問一個對象中的synchronized(this)同步代碼塊時,其他試圖訪問該對象的線程將被阻塞

/**
 * @company: 拓薪教育
 * @author: 大亮老師  QQ:206229531
 */
public class Test1 {


    public static void main(String[] args) {
        Task t = new Task();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        t2.start();
    }

    /**
        線程任務類
     */
    static class Task implements Runnable{

        @Override
        public void run() {
            //同步代碼塊
            synchronized (this){
                try {
                    //休眠一秒
                    TimeUnit.SECONDS.sleep(1);
                    //執行業務邏輯
                    System.out.println(Thread.currentThread().getName()+"正在執行...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

JUC中重入鎖ReentrantLock實現同步鎖

/**
 * @company: 拓薪教育
 * @author: 大亮老師  QQ:206229531
 */
public class Test2 {

    /*
        定義重入鎖
     */
    static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        Task t = new Task();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        t2.start();
    }
    /**
     線程任務類
     */
    static class Task implements Runnable{
        @Override
        public void run() {
            //☆☆☆加鎖☆☆☆
            lock.lock();
            try {
                //休眠1秒
                TimeUnit.SECONDS.sleep(1);
                //執行業務邏輯
                System.out.println(Thread.currentThread().getName()+"正在執行...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //☆☆☆解鎖☆☆☆
                lock.unlock();
            }
        }
    }
}

ReentrantLock和synchronized的不同

  • Synchronized無法響應中斷
  • Synchronized無法得知是否獲得到鎖
  • Synchronized無法控制鎖超時處理
  • 多線程讀操作效率低
  • 默認非公平鎖,無法實現公平鎖

我們可以看下面的例子,我們發現Synchronized是不能被中斷的。

/**
 * @company: 拓薪教育
 * @author: 大亮老師  QQ:206229531
 */
public class Test3 {

    //創建兩個對象(鎖)
    static Object lock1 = new Object();
    static Object lock2 = new Object();
    static int flag = 1;

    /**
     * 定義線程實現類
     */
    static class TxTask implements Runnable {
        //標識屬性
        int flag;
        //構造器
        public TxTask(int flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            //爲了產生死鎖通過flag來讓兩個線程進入不同分支
            if (flag == 1) {
                //持有鎖1
                synchronized (lock1) {
                    System.out.println("進入鎖1");
                    //獲得鎖2
                    synchronized (lock2) {
                        System.out.println("進入鎖1中的鎖2");
                    }
                }
            } else {
                //持有鎖2
                synchronized (lock2) {
                    System.out.println("進入鎖2");
                    //獲得鎖1
                    synchronized (lock1) {
                        System.out.println("進入鎖2中的鎖1");
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        TxTask task = new TxTask(1);
        TxTask task1 = new TxTask(0);
        Thread t = new Thread(task);
        Thread t1 = new Thread(task1);

        t.start();
        t1.start();
        t.interrupt();
    }

}

ReentrantLock可以中斷

public class Test4 {

    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();


    static class Task implements Runnable {
        Lock lock1;
        Lock lock2;

        public Task(Lock lock1, Lock lock2) {
            this.lock1 = lock1;
            this.lock2 = lock2;
        }

        @Override
        public void run() {

            try {
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+"執行邏輯...");
                TimeUnit.MILLISECONDS.sleep(100);
                lock2.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock1.unlock();
                lock2.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Task t = new Task(lock1, lock2);
        Task t1 = new Task(lock2, lock1);

        Thread thread = new Thread(t);
        Thread thread1 = new Thread(t1);
        thread.start();
        thread1.start();

        thread.interrupt();
    }

}

ReentrantLock可以嘗試獲取鎖和控制獲取鎖的超時時間

public class Test5 {

    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();


    static class Task implements Runnable {
        Lock lock1;
        Lock lock2;

        public Task(Lock lock1, Lock lock2) {
            this.lock1 = lock1;
            this.lock2 = lock2;
        }

        @Override
        public void run() {
            try {
                //自旋
                while (!lock1.tryLock()) {
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                System.out.println(Thread.currentThread().getName() + "執行邏輯...");
                TimeUnit.MILLISECONDS.sleep(10);
                while (!lock2.tryLock()) {
                    lock1.unlock();
                    TimeUnit.MILLISECONDS.sleep(10);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock2.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Task t = new Task(lock1, lock2);
        Task t1 = new Task(lock2, lock1);

        Thread thread = new Thread(t);
        Thread thread1 = new Thread(t1);
        thread.start();
        thread1.start();

    }

}

手寫實現排他鎖:方式1

  • 實現流程
    file
  • 代碼實現
public class Test6 {

    static TxLock lock = new TxLock();

    public static void main(String[] args) {
        Task t = new Task();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        t2.start();
    }

    static class Task implements Runnable{
        @Override
        public void run() {
            lock.lock();
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+"正在執行...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    static class TxLock{
        /**
         *  0 表示沒有獲得鎖
         *  1 擁有鎖
         * Unsafe:直接操作內存的類,比較危險,不推薦大家直接操作內存。
         */
        static final Unsafe unsafe;
        //內存上的偏移量
        static final long stateOffset;
        //要在內存上操作的可見變量
        volatile long status = 0;
        static {
            try {
                //使用反射獲取Unsafe的成員變量theUnsafe
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                //設置爲可存取
                field.setAccessible(true);
                //獲取該變量的值
                unsafe = (Unsafe) field.get(null);
                //獲取state在TestUnSafe中的彙編語言偏移量
                stateOffset = unsafe.objectFieldOffset(TxLock.class.getDeclaredField("status"));
            } catch (Exception ex) {
                System.out.println(ex.getLocalizedMessage());
                throw new Error(ex);
            }
        }
        /**
         * CAS
         *
         */
        public void lock(){
            //想要把status改成1
            while (!unsafe.compareAndSwapInt(status, stateOffset, 0, 1)){

            }
        }

        public void unlock(){
            //想要把status改成0
            while (!unsafe.compareAndSwapInt(status, stateOffset, 1, 0)){

            }
        }
    }
}

手寫實現鎖:方式2

  • 實現流程
    file
  • 代碼實現
public class Test7 {

    static TxLock lock = new TxLock();

    public static void main(String[] args) {
        Task t = new Task();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        t2.start();
    }

    static class Task implements Runnable{
        @Override
        public void run() {
            lock.lock();
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName()+"正在執行...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    static class TxLock{

        ConcurrentLinkedQueue<Thread> threads = new ConcurrentLinkedQueue<>();
        /**
         *  0 表示沒有獲得鎖
         *  1 擁有鎖
         *
         */
        static final Unsafe unsafe;
        static final long stateOffset;
        volatile long status = 0;
        static {
            try {
                //使用反射獲取Unsafe的成員變量theUnsafe
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                //設置爲可存取
                field.setAccessible(true);
                //獲取該變量的值
                unsafe = (Unsafe) field.get(null);
                //獲取state在TestUnSafe中的彙編語言偏移量
                stateOffset = unsafe.objectFieldOffset(TxLock.class.getDeclaredField("status"));
            } catch (Exception ex) {
                System.out.println(ex.getLocalizedMessage());
                throw new Error(ex);
            }
        }


        /**
         * CAS
         *
         */
        public void lock(){
            //想要把status改成1
            while (!unsafe.compareAndSwapInt(status, stateOffset, 0, 1)){
                threads.offer(Thread.currentThread());
                LockSupport.park();
            }
        }

        public void unlock(){
            //想要把status改成0
            while (unsafe.compareAndSwapInt(status, stateOffset, 1, 0)){
                Thread thread = threads.poll();
                LockSupport.unpark(thread);
            }
        }
    }
}

ReentrantLock源碼分析

公平鎖內部是FairSync,非公平鎖內部是NonfairSync。而不管是FairSync還是NonfariSync,都間接繼承自AbstractQueuedSynchronizer這個抽象類

  • ReentrantLock分爲公平鎖和非公平鎖,源碼類圖
    file
    該抽象類爲我們的加鎖和解鎖過程提供了統一的模板方法,只是一些細節的處理由該抽象類的實現類自己決定。所以在解讀ReentrantLock(重入鎖)的源碼之前,有必要了解下AbstractQueuedSynchronizer。

AQS以模板方法模式在內部定義了獲取和釋放同步狀態的模板方法,並留下鉤子函數供子類繼承時進行擴展,由子類決定在獲取和釋放同步狀態時的細節,從而實現滿足自身功能特性的需求。除此之外,AQS通過內部的同步隊列管理獲取同步狀態失敗的線程,向實現者屏蔽了線程阻塞和喚醒的細節。

AQS類結構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;

        Node nextWaiter;

AQS中同步等待隊列的實現是一個帶頭尾指針(這裏用指針表示引用是爲了後面講解源碼時可以更直觀形象,況且引用本身是一種受限的指針)且不帶哨兵結點(後文中的頭結點表示隊列首元素結點,不是指哨兵結點)的雙向鏈表。
head是頭指針,指向隊列的首元素;tail是尾指針,指向隊列的尾元素。而隊列的元素結點Node定義在AQS內部,主要有如下幾個成員變量

  • prev:指向前一個結點的指針
  • next:指向後一個結點的指針
  • thread:當前結點表示的線程,因爲同步隊列中的結點內部封裝了之前競爭鎖失敗的線程,故而結點內部必然有一個對應線程實例的引用
  • waitStatus:對於重入鎖而言,主要有3個值。0:初始化狀態;-1(SIGNAL):當前結點表示的線程在釋放鎖後需要喚醒後續節點的線程;1(CANCELLED):在同步隊列中等待的線程等待超時或者被中斷,取消繼續等待。

ReentrantLock加鎖的邏輯代碼

/**
         * 加鎖邏輯
         */
        final void lock() {
            //通過cas方式把state從0更新成1
            if (compareAndSetState(0, 1))
                //獲取鎖成功則將當前線程標記爲持有鎖的線程,然後直接返回
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //如果獲得鎖失敗的後續操作
                acquire(1);
        }

首先嚐試快速獲取鎖,以cas的方式將state的值更新爲1,只有當state的原值爲0時更新才能成功,因爲state在ReentrantLock的語境下等同於鎖被線程重入的次數,這意味着只有當前鎖未被任何線程持有時該動作纔會返回成功。若獲取鎖成功,則將當前線程標記爲持有鎖的線程,然後整個加鎖流程就結束了。若獲取鎖失敗,則執行acquire方法

獲取鎖邏輯

當我們第一次獲取鎖失敗後,我們會嘗試再獲取一次調用acquire(int arg)

/**
     *  tryAcquire(arg) 是AQS中定義的鉤子方法:
     *            該方法默認會拋出異常,強制同步組件通過擴展AQS來實現同步功能的時候必須重寫該方法,ReentrantLock在公平和非公平模式下對此有不同實現
     *  addWaiter() 獲取鎖失敗的線程如何安全的加入同步隊列
     *
     *  acquireQueued()  新節點線程加入同步隊列後掛起
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

該方法主要的邏輯都在if判斷條件中,這裏面有3個重要的方法tryAcquire(),addWaiter()和acquireQueued(),這三個方法中分別封裝了加鎖流程中的主要處理邏輯,理解了這三個方法到底做了哪些事情,整個加鎖流程就清晰了。

非公平鎖獲取鎖

/**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         * 非公平鎖獲取鎖的邏輯
         */
        final boolean nonfairTryAcquire(int acquires) {
            //獲得當前正在運行的線程
            final Thread current = Thread.currentThread();
            //獲取state變量的值,即當前鎖被重入的次數
            int c = getState();
            //state爲0,說明當前鎖未被任何線程持有
            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;
        }

這是非公平模式下獲取鎖的通用方法。它囊括了當前線程在嘗試獲取鎖時的所有可能情況:

1.當前鎖未被任何線程持有(state=0),則以cas方式獲取鎖,若獲取成功則設置exclusiveOwnerThread爲當前線程,然後返回成功的結果;若cas失敗,說明在得到state=0和cas獲取鎖之間有其他線程已經獲取了鎖,返回失敗結果。
2.若鎖已經被當前線程獲取(state>0,exclusiveOwnerThread爲當前線程),則將鎖的重入次數加1(state+1),然後返回成功結果。因爲該線程之前已經獲得了鎖,所以這個累加操作不用同步。
3.若當前鎖已經被其他線程持有(state>0,exclusiveOwnerThread不爲當前線程),則直接返回失敗結果
因爲我們用state來統計鎖被線程重入的次數,所以當前線程嘗試獲取鎖的操作是否成功可以簡化爲:state值是否成功累加1,是則嘗試獲取鎖成功,否則嘗試獲取鎖失敗。

創建node節點邏輯

tryAcquire(arg)返回成功,則說明當前線程成功獲取了鎖(第一次獲取或者重入),由取反和&&可知,整個流程到這結束,只有當前線程獲取鎖失敗纔會執行後面的判斷。先來看addWaiter(Node.EXCLUSIVE)
部分,這部分代碼描述了當線程獲取鎖失敗時如何安全的加入同步等待隊列。這部分代碼可以說是整個加鎖流程源碼的精華,充分體現了併發編程的藝術性。

private Node addWaiter(Node mode) {
        //首先創建一個新節點,並將當前線程實例封裝在內部,mode這裏爲null
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //嘗試快速入隊
        //定義尾部臨時節點
        Node pred = tail;
        //如果尾部不是空
        if (pred != null) {
            //把新節點的前指針設置成尾節點
            node.prev = pred;
            //CAS方式把新節點設置尾節點
            if (compareAndSetTail(pred, node)) {
                //把老的尾節點的尾指針設置成新節點
                pred.next = node;
                //返回新階段
                return node;
            }
        }
        //自旋入隊
        enq(node);
        return node;
    }

首先創建了一個新節點,並將當前線程實例封裝在其內部,之後我們直接看enq(node)方法就可以了,中間這部分邏輯在enq(node)中都有,之所以加上這部分“重複代碼”和嘗試獲取鎖時的“重複代碼”一樣,對某些特殊情況
進行提前處理,犧牲一定的代碼可讀性換取性能提升。

  • 創建node節點自旋入隊
private Node enq(final Node node) {
        //
        for (;;) {
            //定義尾部臨時節點
            Node t = tail;
            //首節點情況
            if (t == null) { // Must initialize
                //當前階段CAS方式作爲頭階段
                if (compareAndSetHead(new Node()))
                    //同時尾階段也設置成新節點
                    tail = head;
            } else {
                //新節點的前指針設置成舊的尾節點
                node.prev = t;
                //CAS方式設置尾節點
                if (compareAndSetTail(t, node)) {
                    //舊的尾節點指向新節點
                    t.next = node;
                    return t;
                }
            }
        }
    }

這裏有兩個CAS操作:

compareAndSetHead(new Node()),CAS方式更新head指針,僅當原值爲null時更新成功

//當前階段CAS方式作爲頭階段
                if (compareAndSetHead(new Node()))
                    //同時尾階段也設置成新節點
                    tail = head;

外層的for循環保證了所有獲取鎖失敗的線程經過失敗重試後最後都能加入同步隊列。因爲AQS的同步隊列是不帶哨兵結點的,故當隊列爲空時要進行特殊處理,這部分在if分句中。注意當前線程所在的結點不能直接插入
空隊列,因爲阻塞的線程是由前驅結點進行喚醒的。故先要插入一個結點作爲隊列首元素,當鎖釋放時由它來喚醒後面被阻塞的線程,從邏輯上這個隊列首元素也可以表示當前正獲取鎖的線程,雖然並不一定真實持有其線程實例。

首先通過new Node()創建一個空結點,然後以CAS方式讓頭指針指向該結點(該結點並非當前線程所在的結點),若該操作成功,則將尾指針也指向該結點。
compareAndSetTail(t, node),CAS方式更新tial指針,僅當原值爲t時更新成功

if (compareAndSetTail(t, node)) {
                    //舊的尾節點指向新節點
                    t.next = node;
                    return t;
                }

首先當前線程所在的結點的前向指針pre指向當前線程認爲的尾結點,源碼中用t表示。然後以CAS的方式將尾指針指向當前結點,該操作僅當tail=t,即尾指針在進行CAS前未改變時成功。若CAS執行成功,則將原尾結點的後向指針next指向新的尾結點。
整個入隊的過程並不複雜,是典型的CAS加失敗重試的樂觀鎖策略。其中只有更新頭指針和更新尾指針這兩步進行了CAS同步,可以預見高併發場景下性能是非常好的。

節點掛起和獲得執行邏輯

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沒有發生,即沒有獲得鎖要掛起
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //如果失敗
            if (failed)
                //取消獲得鎖,資源回收
                cancelAcquire(node);
        }
    }

這段代碼主要的內容都在for循環中,這是一個死循環,主要有兩個if分句構成。第一個if分句中,當前線程首先會判斷前驅結點是否是頭結點,如果是則嘗試獲取鎖,獲取鎖成功則會設置當前結點爲頭結點(更新頭指針)。爲什麼必須前驅結點爲頭結點才嘗試去獲取鎖?因爲頭結點表示當前正佔有鎖的線程,正常情況下該線程釋放鎖後會通知後面結點中阻塞的線程,阻塞線程被喚醒後去獲取鎖,這是我們希望看到的。然而還有一種情況,就是前驅結點取消了等待,此時當前線程也會被喚醒,這時候就不應該去獲取鎖,而是往前回溯一直找到一個沒有取消等待的結點,然後將自身連接在它後面。一旦我們成功獲取了鎖併成功將自身設置爲頭結點,就會跳出for循環。否則就會執行第二個if分句:確保前驅結點的狀態爲SIGNAL,然後阻塞當前線程。

先來看shouldParkAfterFailedAcquire(p, node)判斷是否要阻塞當前線程的

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //獲得前節點的等待狀態
        int ws = pred.waitStatus;
        //如果前節點是正常等待喚醒狀態返回true
        if (ws == Node.SIGNAL)
            return true;
            //前一個線程被取消等待或者超時
        if (ws > 0) {
            do {
                //向前追溯前面一個正常等待的節點
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            //正常前節點尾指針指向當前新節點
            pred.next = node;
        } else {
            //初識設置
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

可以看到針對前驅結點pred的狀態會進行不同的處理

1.pred狀態爲SIGNAL,則返回true,表示要阻塞當前線程。
2.pred狀態爲CANCELLED,則一直往隊列頭部回溯直到找到一個狀態不爲CANCELLED的結點,將當前節點node掛在這個結點的後面。
3.pred的狀態爲初始化狀態,此時通過compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法將pred的狀態改爲SIGNAL。
其實這個方法的含義很簡單,就是確保當前結點的前驅結點的狀態爲SIGNAL,SIGNAL意味着線程釋放鎖後會喚醒後面阻塞的線程。畢竟,只有確保能夠被喚醒,當前線程才能放心的阻塞。

但是要注意只有在前驅結點已經是SIGNAL狀態後纔會執行後面的方法立即阻塞,對應上面的第一種情況。其他兩種情況則因爲返回false而重新執行一遍
for循環。

shouldParkAfterFailedAcquire返回true表示應該阻塞當前線程,則會執行parkAndCheckInterrupt方法,這個方法比較簡單,底層調用了LockSupport來阻塞當前線程,源碼如下:

private final boolean parkAndCheckInterrupt() {
        //阻塞當前線程
        LockSupport.park(this);
        //返回中斷標誌
        return Thread.interrupted();
    }

更詳細的Spring源碼解析請關注:java架構師免費課程
每晚20:00直播分享高級java架構技術
掃描加入QQ交流羣264572737
在這裏插入圖片描述

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