ReentrantLock可重入鎖源碼原理詳解

ReentrantLock可重入鎖源碼原理詳解

背景介紹

AbstractQueuedSynchronizer是Doug Lea在JDK1.5的時候加入的一個同步框架,也被簡稱爲AQS,該框架主要維護了被競爭資源的狀態,和獲取到資源的線程(經過AbstractOwnableSynchronizer來維護)以及未獲取到資源的線程的管理,AQS主要經過volatile的內存可見性和CAS來實現。具體的競爭資源的方式(公平、非公平)由子類實現,Doug Lea在引入該框架時提供了一系列已經實現好的子類,好比:ReentrantLock、ReentrantReadWriteLock,爲了對使用者透明具體實現細節下降使用門檻,這兩個鎖自己並不繼承AQS,而是由內部類Sync繼承AQS並經過組合的方式實現鎖。java

ReentrantLock介紹

AQS實現了共享方式和排它方式,而ReentrantLock只對外暴露出了AQS的排它方式,因此ReentrantLock也叫作排它鎖,在這個基礎上ReentrantLock又經過兩個內部類(FairSync、NonfairSync)間接繼承了AQS分別實現了公平鎖、非公平鎖node

在這裏插入圖片描述

ReentrantLock與synchronized對比

1. 競爭鎖標識安全

​ synchronized: 線程經過獲取某個對象的Monitor的全部權框架

​ ReentrantLock: 線程經過修改AQS中的被volatile修飾的int類型的state變量ide

2. 搶到鎖標識的線程以及未搶到鎖標識的線程維護函數

​ synchronized: 不清楚具體實現(跟Monitor Record有關),不瞭解JVM是如何維護的。性能

​ ReentrantLock: 經過AQS的基類AbstractOwnableSynchronizer中的一個成員變量來記錄獲取到資源的線程,並把爲獲取到鎖標識的線程維護在一個FIFO的隊列中。測試

Monitor Record內部結構:ui

Monitor Record
Owner 初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程惟一標識,當鎖被釋放時又設置爲NULL;
EntryQ 關聯一個系統互斥鎖(semaphore),阻塞全部試圖鎖住monitor record失敗的線程。
RcThis 表示blocked或waiting在該monitor record上的全部線程的個數。
Nest 用來實現重入鎖的計數。
HashCode 保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
Candidate 用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。

3. 線程通訊this

synchronized: 經過某個對象的wait()、notify()、notifyAll()方法來實現。 注: 該方法須要在synchronized 語句塊中調用不然會拋IllegalMonitorStateException異常。

ReentrantLock:經過await()、signal()、signalAll()方法來實現(區別於wait()、notify()、notifyAll())。 注: 該方法須要在ReentrantLock調用lock()方法後,unlock()方法前調用,不然會拋IllegalMonitorStateException異常。

4. 未搶到鎖標識的線程狀態

synchronized: 線程處於BLOCKED狀態。

ReentrantLock: 線程處於WAITING狀態。
下文證實

5.鎖類型

synchronized: 只有非公平鎖實現。

ReentrantLock: 既有非公平鎖又有公平鎖,默認爲非公平鎖。

ReentrantLock(非公平鎖)實現源碼

首先看一下ReentrantLock的構造函數:

public ReentrantLock() { 
        sync = new NonfairSync();
    }
public ReentrantLock(boolean fair) { 
        sync = fair ? new FairSync() : new NonfairSync();
    }

1.獲取鎖(採用模板方法模式)

獲取鎖的過程主要採用模板方法模式,流程看起來感受會有點亂。

  1. 獲取鎖的入口是ReentrantLock類中的lock()方法,該方法會調用內部抽象類的lock()方法

    public void lock() { 
            sync.lock();    //sync爲ReentrantLock的內部抽象類繼承AQS
        }
  2. 內部抽象類Sync的lock()方法爲抽象方法,該方法的具體實現由ReentrantLock的內部類NonfairSync實現

    abstract void lock();
  3. ReentrantLock的lock()方法最終實現是在NonfairSync的lock()方法,一進入該方法先去競爭鎖標識,這裏也是非公平的緣由之一。

    final void lock() { 
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }

    ① 獲取鎖的時候先經過CAS競爭鎖標識,若是成功把AQS中的state成員變量從0修改成1就認爲本身(當前線程)成功。(方法簡單不展開)

    獲取到鎖標識並記錄到AbstractOwnableSynchronizer中的成員變量exclusiveOwnerThread中,線程繼續向下執行。(方法簡單不展開)

    ③ 若是①失敗表示該線程未獲取到鎖標識則進入AQS的acquire()方法。

  4. AQS的acquire()方法

    public final void acquire(int arg) {     //該方法採用模板方法模式
            if (!tryAcquire(arg) &&         
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        
                selfInterrupt();
        }

    該方法採用模板方法模式,其中tryAcquire()方法嘗試獲取鎖標識具體實現由子類實現,若是獲取鎖標識成功線程繼續向下執行,若是獲取失敗,線程將會進入等待狀態(不是阻塞狀態跟synchronized不一樣,以下圖),而後將該線程構建成一個獨佔式的節點放到隊列中進行維護。

    如圖(分析兩個線程經過ReentrantLock鎖成功和失敗的線程狀態):

    示例代碼:

    public static void main(String[] args) throws ExecutionException, InterruptedException { 
    
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Thread t1 = new Thread(new Runnable() { 
                @Override
                public void run() { 
    
                    try { 
                        reentrantLock.lock();
                        while (true) { 
                        }
                    } finally { 
                        reentrantLock.unlock();
                    }
                }
            }, "T1");
            t1.start();
    
            Thread t2 = new Thread(new Runnable() { 
                @Override
                public void run() { 
    
                    try { 
                        reentrantLock.lock();
                        while (true) { 
                        }
                    } finally { 
                        reentrantLock.unlock();
                    }
                }
            }, "T2");
            t2.start();
    
            t1.join();
            t2.join();
    
        }

    上述事例代碼很簡單,兩個線程進行爭搶同一把鎖,而後經過jstack分析T一、T2線程所處的狀態:

在這裏插入圖片描述

能夠清晰的看到T1線程獲取到了鎖標識處於RUNNABLE(JVM將操做系統的READY就緒狀態和RUNNING運行中狀態合併爲RUNNABLE狀態)狀態,T2線程並未獲取到鎖標識處於WAITING狀態(並非BLOCKED狀態緣由是由於底層調用LockSupport.park()方法)。

  1. 若是競爭鎖標識失敗將會進入addWaiter(Node.EXCLUSIVE)方法:

    private Node addWaiter(Node mode) { 
            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;
                if (compareAndSetTail(pred, node)) { 
                    pred.next = node;
                    return node;
                }
            }
            enq(node);
            return node;
        }

    該方法把當前線程構建成一個獨佔式的Node節點放到FIFO隊列中,該方法先判斷隊尾是否爲空,若是不爲空經過CAS將該線程的節點放到隊尾而後返回若CAS失敗則進入enq()方法,若是隊尾爲空則證實該隊列還沒有初始化,則進入enq()方法初始化隊列並將該節點放入隊列中,具體向下看enq()方法實現。

  2. enq()方法分析:

    private Node enq(final Node node) { 
            for (;;) { 
                Node t = tail;
                if (t == null) {  // Must initialize
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else { 
                    node.prev = t;
                    if (compareAndSetTail(t, node)) { 
                        t.next = node;
                        return t;
                    }
                }
            }
        }

    能夠看到該方法是個死循環(CAS的失敗重試),用來保證該線程節點放入隊尾,第一個判斷用來判斷該隊列是否已經初始化過,若還沒有初始化則先進性初始化操做,而後在經過CAS失敗重試將該節點放入隊尾並返回。

  3. 再繼續分析將該節點放入隊列中的後續操做,將會執行acquireQueued()方法:

    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())      //主要調用LockSupport.park()方法阻塞當前線程,並在當前線程醒來時重 置該線程的中斷標誌位
                        interrupted = true;
                }
            } finally { 
                if (failed)
                    cancelAcquire(node);
            }
        }

① 首先該方法將會進入一個死循環,若是該節點的父節點是對頭(只有對頭的節點才持有鎖標識),若是該節點的父節點是頭結點則證實該節點可能(下文宏觀獲取鎖流程講解爲何是可能) 立刻就能夠獲取到鎖標識了,進行tryAcquire()嘗試獲取鎖標識,若是獲取成功,把該節點設置爲頭結點,並返回false(主要是返回一個暗號,不讓當前線程設置中斷標識[下文講解線程中斷])。

② 若是該節點的父節點不是頭結點(說明下一個獲取到鎖的線程必定不是他),或者是頭結點可是競爭鎖標識失敗了(下文宏觀獲取鎖流程講解爲何會失敗),將會進入shouldParkAfterFailedAcquire()方法,該方法主要判斷該節點是否能夠安全的進行阻塞,還有其餘處理邏輯,若是能夠安全阻塞將會(觸發LockSupport.park()方法進入WAITING狀態)阻塞該線程。

③ 能夠發現上述流程發生在一個死循環中,通常狀況會等到獲取到鎖標識後正常返回,不過肯能存在幾種狀況在爲獲取到鎖以後就返回了,不如線程取消,等待超時,將會進入cancelAcquire()方法(該方法還沒來得及好好看,好像主要是針對這些未正常獲取到鎖就返回的線程的處理以及對存放線程Node節點的隊列的一些維護操做)。

④ 正常返回的狀況下會返回true或者false,若是返回false證實該線程並無進入WAITING狀態,若是返回true則說明該線程進入過WAITING狀態並在甦醒時對線程的中斷標誌位進行置位。後續操做會根據這個返回結果對該線程的中斷標誌位進行相應的設置。

注:線程中斷標誌位好像在本流程中沒用,好像在其餘流程中用到了,好比tryAcquireNanos()方法,該方法好像會相應線程中斷。

  1. 還有一個重要的方法就是tryAcquire()方法:

    protected final boolean tryAcquire(int acquires) { 
                return nonfairTryAcquire(acquires);
            }
    final boolean nonfairTryAcquire(int acquires) { 
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) { 
                    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;
            }

    該方法很重要不少地方都用到了,可是也很簡單。

    ① 首先獲取當前線程,而且獲取鎖標識的值(0標識沒有線程持有鎖)。

    ② 當鎖標識爲0的時候標識沒有線程持有鎖,使用CAS競爭鎖標識,若是競爭成功則把當前線程記錄到AbstractOwnableSynchronizer的成員變量中。

    ③ 若是鎖標識不爲0,則說明有線程持有該鎖,而後判斷持有鎖的線程是不是當前線程,若是是則將鎖標識位+1(這就是爲何說ReentrantLock是一把可重入鎖)。

獲取鎖(非公平鎖)的流程結束,釋放鎖的流程很簡單,你們有興趣能夠本身去看源碼(實在是不想寫了)

線程中斷

我的理解:線程中斷是一種線程間進行通訊的方式之一,他自己是線程的一個屬性,用來標識其餘線程(也能夠是他自己)給該線程(運行中)的中斷標識位設置值(好像是一個boolean值),至關於其餘線程給該線程發送的一個消息。

注:該線程必須處於運行中才能收到該線程的中斷消息,好比該線程處於WAITING狀態沒法收到中斷消息。

宏觀獲取鎖流程

首先看一下公平鎖和非公平鎖表現的結果:

測試代碼:

public class ReentrantLockTest { 

    //須要繼承ReentrantLock把getQueuedThreads()方法暴露出來
    public class MyReentrantLock extends ReentrantLock{ 

        public MyReentrantLock(boolean fair) { 
            super(fair);
        }

        @Override
        public Collection<Thread> getQueuedThreads() { 
            List<Thread> list = new ArrayList<>(super.getQueuedThreads());
            //因爲是逆序輸出的因此進行翻轉,不信能夠看輸入線程隊列的源碼
            Collections.reverse(list);
            return list;
        }
    }

    public static class Job extends Thread{ 

        public MyReentrantLock reentrantLock;
        public Job(MyReentrantLock reentrantLock){ 
            this.reentrantLock = reentrantLock;
        }
        @Override
        public void run() { 

            for (int i = 0; i < 2; i++){ 
                reentrantLock.lock();
                List<String> collect = reentrantLock.getQueuedThreads().stream().map(e -> { 
                    return e.getName();
                }).collect(Collectors.toList());

                System.out.println("當前線程:"+Thread.currentThread().getName()+",阻塞隊列線程:"+collect);

                try { 
                    TimeUnit.MILLISECONDS.sleep(20);
                }catch (Exception e){ }
                reentrantLock.unlock();
            }
        }
    }

    @Test   //非公平鎖測試
    public void testNotFair() throws InterruptedException { 
        MyReentrantLock myReentrantLock = new MyReentrantLock(false);
        for (int i=0;i<5;i++){ 
            Job job = new Job(myReentrantLock);
            job.setName(i+"");
            job.start();
        }
        Thread.currentThread().join(2000);
    }

    @Test   //公平鎖測試
    public void testFair() throws InterruptedException { 
        MyReentrantLock myReentrantLock = new MyReentrantLock(true);
        for (int i=0;i<5;i++){ 
            Job job = new Job(myReentrantLock);
            job.setName(i+"");
            job.start();
        }
       Thread.currentThread().join(2000);
    }
}

結果:

非公平鎖testNotFair():

在這裏插入圖片描述

公平鎖testFair():

在這裏插入圖片描述

對比結果能夠看到以下結果:

非公平鎖:每當一個線程搶到鎖以後,再來的其餘線程將會進入到FIFO的隊列中進行排隊,當獲取到鎖的線程釋放鎖以後,本應該隊列中的下一個節點線程獲取鎖,可是若是有新線程(沒有在隊列中的線程)來搶鎖,那麼新線程將會和隊列中的頭結點的下一個線程爭搶鎖標識,這就是表現出來的不公平的地方,可是仔細觀察結果會發現不公平中也存在的公平,只要線程進入隊列中以後,將會在隊列中按照先進先出的順序進行獲取鎖。

公平鎖:每當一個線程搶到鎖以後,再來的其餘線程不會嘗試搶鎖,先回去判斷線程隊列中是否還有線程在進行排隊獲取鎖,若是有本身將會去隊列中排隊獲取鎖,從結果來看,線程0釋放鎖以後將會去隊列末尾排隊,線程1釋放鎖後一樣回去排隊,以此類推。

這就解決了上述的問題,若是本文章有什麼不對或者不合理的地方,但願大佬指正。

相關文章
相關標籤/搜索