併發包源碼解讀——ReentrantLock

1、構造器

ReentrantLock可以構造兩種鎖:公平鎖和非公平鎖

我們可以先複習一下概念

1.1、非公平鎖

每個等待中的線程都有相等的機率搶佔到鎖(不排隊,可以插隊,所以不公平)

優點:吞吐量高,因爲在鎖被其他資源搶佔時,公平鎖需要將每個新來的線程依次掛起放入等待隊列,而非公平鎖有機率使新來的線程直接獲取到鎖,性能的差異就在於新線程的掛起和喚醒

缺點:在等待隊列中的線程有可能長時間獲取不到鎖,處於飢餓狀態

默認構建非公平鎖

public ReentrantLock() {
    sync = new NonfairSync();
}

1.2、公平鎖

先進入等待隊列的線程先獲取鎖

優缺點與非公平鎖相反,吞吐量差點,但是線程不會出現飢餓

傳參構造非公平鎖

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

至於兩者具體的實現差別在哪,往下看就知道了

2、非公平鎖實現邏輯

我們以最簡單的demo開始:

主線程獲取鎖對象,然後創建一個子線程再嘗試獲取鎖對象,如果主線程不解鎖,那子線程就會一直處於park狀態

public static void main(String[] args) throws InterruptedException {
    ReentrantLock reentrantLock = new ReentrantLock();
    reentrantLock.lock();
    Thread.sleep(1000);
    new Thread(() -> reentrantLock.lock()).start();
}

2.1、非公平鎖構造器

NonfairSync這個類繼承了Sync,Sync又繼承了AbstractQueuedSynchronizer

AQS這個類很著名也很重要,裏面有很多成員變量和方法與CLH隊列的實現有關,所以可以認爲一個鎖對象對應一個AQS,在下面的實現邏輯中會串進去講

2.2、非公平鎖獲取鎖

可以看到,線程在第一次獲取非公平鎖的時候,什麼也不顧及直接先嚐試CAS獲取鎖,對比公平鎖會直接調用acquire,後面講

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
2.2.1、compareAndSetState——獲取鎖

加鎖的時候,會先進到compareAndSetState方法中,這個方法就是給鎖對象加鎖,原理是嘗試使用CAS改變鎖對象的狀態,方法在AQS中實現,是unsafe中對應方法的封裝。unsafe裏都是native方法,跟java的底層c庫有關,就先不拓展講了(主要我也沒看過)

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

這個stateOffset是個靜態變量,放在static代碼塊中,在new第一個鎖的時候就已經初始化,可以理解爲存儲state字段的內存地址與AQS起始地址的偏移量。同時還初始化了其他偏移量,都是底層用來尋址的。這些靜態變量並不影響AQS對象的內部狀態。

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

static {
    try {
        stateOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
        headOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
        tailOffset = unsafe.objectFieldOffset
            (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
        waitStatusOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("waitStatus"));
        nextOffset = unsafe.objectFieldOffset
            (Node.class.getDeclaredField("next"));

    } catch (Exception ex) { throw new Error(ex); }
}

下面是AQS的前3個成員變量,可以看見state之前有兩個對象,64位JVM虛擬機用8字節存儲一個對象引用,所以你debug的時候可以看到stateOffset初始化之後值就是16

    private transient volatile Node head;

    private transient volatile Node tail;

    private volatile int state;

2.2.2、setExclusiveOwnerThread——設置擁有鎖的線程

加鎖成功會執行setExclusiveOwnerThread,把有鎖的線程緩存起來

protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

2.2.3、acquire——嘗試重入鎖和排隊

2.2.3.1、重入鎖

可重入鎖的概念就是,同一個線程可以在不釋放鎖的情況下,多次獲取同一把鎖,下面來看邏輯

加鎖失敗後會進入acquire方法

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

進入tryAcquire方法,它是nonfairTryAcquire的封裝

首先它會再嘗試獲取一次鎖(之前CAS已經失敗了,現在又試一次,兩次獲取鎖之間的代碼只有Thread.currentThread(),不知道是不是爲了極限性能,提高非公平性),成功了就返回“獲取成功”

失敗了就判斷持有鎖的線程是不是自己,是自己就給state加1,表示加鎖的次數,這就是可重入鎖的實現邏輯

如果擁有鎖的線程都不是自己的,就返回“獲取失敗”

對比公平鎖的實現,少了一步hasQueuedPredecessors判斷隊列中是否有等待的線程,公平鎖的具體實現我們也後面再講

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

當這次嘗試獲取鎖成功了,方法就結束了,獲取失敗了會怎麼樣呢

2.2.3.2、排隊

會進入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)這個方法

首先來看addWaiter方法,他的作用是向隊列中加入排隊線程的節點

如果隊列裏有尾節點,就把該線程的節點與尾節點關聯上然後返回

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

反之進入enq方法,這個方法是用來初始化隊列首尾節點的,跟LinkedList結構有點像

其中compareAndSetHead和compareAndSetTail也都是調用unsafe方法,傳AQS對象和對應參數的偏移量,套路相同

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

從addWaiter出來之後,返回了當前加入隊列的節點,就是想要獲取鎖的該線程所對應的節點,當作參數進入acquireQueued方法中

acquireQueued這個方法是真正讓線程進入阻塞狀態的方法

在進入for循環後,會進入shouldParkAfterFailedAcquire方法檢查前一個節點的waitStatus並置爲-1,表示該節點(其實原意應該是前一個節點後面的節點,意思都一樣)應該park,然後在第二次循環時執行parkAndCheckInterrupt

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

parkAndCheckInterrupt是LockSupport.park的封裝

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

線程停就停在了UNSAFE.park這個方法,等上一個獲取鎖的線程釋放鎖,這個線程就能從park中解脫出來,繼續往下走

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

線程從park中解脫之後,會回到acquireQueued方法,在下一次for循環中進入(p == head && tryAcquire(arg))這個條件,嘗試獲取鎖,獲取到了就返回,沒獲取到就繼續之前幾步。其中會判斷線程的interrupted中斷狀態,進行自我中斷

(這裏也補充一點,因爲在線程進入隊列之後就不會被中斷,外部調用該線程的中斷方法,該線程也不會立刻響應,只會給該線程做一個“需要中斷”的標記並喚醒該線程,並讓該線程從UNSAFE.park中解脫出來,然後再次進入acquireQueued的for循環中,等該線程獲取到鎖之後再自己調用中斷)

2.2.3、非公平鎖釋放鎖

釋放鎖調用unlock方法,是release方法的封裝

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

首先調用tryRelease方法釋放鎖

首先該線程肯定得是鎖的擁有者,然後鎖狀態-1並更新,如果-1之後鎖狀態爲0說沒有重入鎖了,就把線程擁有者置空

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

回到release方法,如果頭節點的waitStatus不是0,說明還有線程在park狀態,需要被喚醒,執行unparkSuccessor方法

可以看到,unparkSuccessor傳進來的參數是隊列的虛擬頭節點,所以進入隊列的線程對鎖的獲取都是公平的,都是順序喚醒的

當虛擬頭節點的下一個節點無效的時候,就無法再順序找到下一個有效的節點,所以會從尾節點往前找,然後找到第一個有效的節點

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

釋放鎖或喚醒之後,各個線程就會繼續執行他們後面的邏輯,非公平鎖的部分就告一段落了

3、公平鎖實現邏輯

那我們主要說與非公平鎖的實現差異

3.1、構造器

傳true構造公平鎖

ReentrantLock reentrantLock = new ReentrantLock(true);

3.2、獲取鎖

直接調用acquire方法,而非公平鎖會先嚐試CAS獲取一次鎖

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

tryAcquire的實現不同

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,用來檢查隊列中是否有park線程,保證先來的線程先獲取鎖,實現線程獲取鎖的公平性

public final boolean hasQueuedPredecessors() {
    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());
}

其餘地方與非公平鎖調用的都是相同的方法

以上就是ReetrantLock源碼的解讀,講的不清楚的或者有問題的可以留言,互相討論互相進步,同時歡迎點贊

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