啃碎JDK源碼(八):ReentrantLock

前言

上一次咱們已經講了AQS,若是對其不熟悉的話建議先去看看其實現原理,看完再來看ReentrantLock就很簡單了。html

啃碎JDK源碼(一):String
啃碎JDK源碼(二):Integer
啃碎JDK源碼(三):ArrayList
啃碎JDK源碼(四):HashMap
啃碎JDK源碼(五):ConcurrentHashMap
啃碎JDK源碼(六):LinkedList
啃碎JDK源碼(七):AbstractQueuedSynchronizer(AQS)java

ReentrantLockSynchornized 在面試中常常被用來比較,若是想了解Synchronized的話能夠看我另一篇文章:死磕Synchronized面試

正文

先來了解一下一些核心屬性:segmentfault

public class ReentrantLock implements Lock, java.io.Serializable {
// 實現AQS的內部類
private final Sync sync;
  ......
}

沒錯,ReentrantLock沒有什麼值得注意的屬性,由於已經在AQS中定義好了,咱們只須要繼承它而後進行簡單的實現便可。性能

先看下 ReentrantLock 的用法:ui

public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            // 執行業務
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

只要調用 lock 方法就能夠進行加鎖操做,表示接下來的這段代碼已經被當前線程鎖住,其餘線程須要執行時須要拿到這個鎖才能執行,而當前線程在執行完以後要顯式的調用 unlock 釋放鎖。spa

注意:看源碼以前你必需要對AQS比較熟悉才行,能夠參考我上一篇博客:
啃碎JDK源碼(七):AbstractQueuedSynchronizer(AQS)操作系統

咱們來跟進源碼看一下,先來看咱們的加鎖lock方法:線程

public void lock() {
      sync.lock();
  }
  
  // Sync繼承了AQS
  abstract static class Sync extends AbstractQueuedSynchronizer {
  
     abstract void lock();
     ......
  }

能夠看到是調用內部類的lock方法,而它是一個抽象方法,咱們看下誰繼承了這個抽象接口:code

image.png

FairSyncNonfairSyncReentrantLock 的另外兩個內部類。顧名思義一個是公平鎖,一個是非公平鎖。(公平鎖就是永遠都是隊列的第一位才能獲得鎖

AQS有一個同步隊列(CLH),是一種先進先出隊列。公平鎖的意思就是嚴格按照這個隊列的順序來獲取鎖,非公平鎖的意思就是不必定按照這個隊列的順序來。

在new對象的時候便會對sync初始化,以下:

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

能夠看出默認是非公平鎖,若是傳true則初始化爲公平鎖。

那咱們首先來看看非公平鎖:

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            // CAS修改狀態
            if (compareAndSetState(0, 1))
                // 設置獨佔線程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 進入隊列等待
                acquire(1);
        }
        // tryAcquire是AQS的抽象方法,咱們這裏對其實現
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

首先用 compareAndSetState 方法使用CAS修改state狀態變量的值,若是修改爲功的話使用 setExclusiveOwnerThread(Thread.currentThread()) 方法將當前線程設置爲獨佔鎖的持有線程,不然調用AQS的 acquire 方法進去隊列等待處理。

接下來看一下acquire方法:

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

該方法是AQS裏的方法,咱們上次已經介紹過了,這裏直接截過來看下:

image.png

此次咱們主要關注由子類ReentrantLock實現的tryAcquire方法:

image.png

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");
                // 直接修改state遍歷,由於已經持有鎖,不須要用CAS去修改
                setState(nextc);
                return true;
            }
            return false;
        }

上面代碼和咱們在上次手動實現一個可重入鎖的代碼差很少,這裏就再也不展開。

那接下來看一下 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;
    }

release 方法在AQS類中定義好了,咱們子類主要實現 tryRelease 方法:

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;
        }

這段代碼上篇文章咱們也已經講過了,若是忘記的同窗能夠回頭看看。

看完非公平鎖的最後來看看公平鎖的加鎖方法:

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;
        }
    }

其實代碼基本和前面同樣,只是多了hasQueuedPredecessors方法用來判斷是否存在比等待更久的線程,由於要按照等待時間順序獲取資源,其它的這裏就再也不細說了。

其它疑問

如下問題來自 從源碼角度理解ReentrantLock

爲何基於FIFO的同步隊列能夠實現非公平鎖?

由FIFO隊列的特性知,先加入同步隊列等待的線程會比後加入的線程更靠近隊列的頭部,那麼它將比後者更早的被喚醒,它也就能更早的獲得鎖。從這個意義上,對於在同步隊列中等待的線程而言,它們得到鎖的順序和加入同步隊列的順序一致,這顯然是一種公平模式。然而,線程並不是只有在加入隊列後纔有機會得到鎖,哪怕同步隊列中已有線程在等待,非公平鎖的不公平之處就在於此。回看下非公平鎖的加鎖流程,線程在進入同步隊列等待以前有兩次搶佔鎖的機會:

  • 第一次是使用compareAndSetState方法嘗試修改state變量,只有在當前鎖未被任何線程佔有(包括自身)時才能成功。
  • 第二次是在進入同步隊列前使用tryAcquire(arg)嘗試獲取鎖。

只有這兩次獲取鎖都失敗後,線程纔會構造結點並加入同步隊列等待。而線程釋放鎖時是先釋放鎖(修改state值),而後才喚醒後繼結點的線程的。試想下這種狀況,線程A已經釋放鎖,但還沒來得及喚醒後繼線程C,而這時另外一個線程B恰好嘗試獲取鎖,此時鎖剛好不被任何線程持有,它將成功獲取鎖而不用加入隊列等待。線程C被喚醒嘗試獲取鎖,而此時鎖已經被線程B搶佔,故而其獲取失敗並繼續在隊列中等待。
那咱們在開發中爲何大多使用非公平鎖?很簡單,由於它性能好啊。

爲何非公平鎖性能好

  1. 線程沒必要加入等待隊列就能夠得到鎖,不只免去了構造結點並加入隊列的繁瑣操做,同時也節省了線程阻塞喚醒的開銷,線程阻塞和喚醒涉及到線程上下文的切換和操做系統的系統調用,是很是耗時的。。
  2. 減小CAS競爭。若是線程必需要加入阻塞隊列才能獲取鎖,那入隊時CAS競爭將變得異常激烈,CAS操做雖然不會致使失敗線程掛起,但不斷失敗重試致使的對CPU的浪費也不能忽視。

總結

有關 ReentrantLock的知識就介紹到這裏了,有什麼不對的地方請多多指教。

image.png

相關文章
相關標籤/搜索