首頁  >  文章  >  Java  >  詳解Java中可重入鎖ReentrantLock原理的範例程式碼

詳解Java中可重入鎖ReentrantLock原理的範例程式碼

黄舟
黄舟原創
2017-03-22 11:07:291820瀏覽

一、 概述

本文首先介紹Lock介面、ReentrantLock的類別層次結構以及鎖定功能模板類別AbstractQueuedSynchronizer的簡單原理,然後透過分析ReentrantLock的lock方法和unlock方法,來解釋ReentrantLock的內部原理,最後做一個總結。本文不涉及ReentrantLock中的條件變數。

1.1、Lock介面

Lock接口,是控制並發的工具的抽象。它比使用synchronized關鍵字更靈活,並且能夠支援條件變數。它是一種控制並發的工具,一般來說,它控制對某種共享資源的獨佔。也就是說,同一時間內只有一個執行緒可以取得這個鎖定並佔用資源。其他執行緒想要取得鎖,必須等待這個執行緒釋放鎖。在Java實作中的ReentrantLock就是這樣的鎖。另外一種鎖,它可以允許多個執行緒讀取資源,但是只能允許一個執行緒寫入資源,ReadWriteLock就是這樣一種特殊的鎖,簡稱讀寫鎖。以下是Lock介面的幾個方法的總體描述:

##lockInterruptibly取得鎖,除非當前執行緒被中斷。如果獲取到了鎖,那麼立即返回,如果獲取不到,那麼當前線程變得不可被調度,一直休眠直到下面兩件事情發生:tryLock如果呼叫的時候能夠取得鎖,那麼就取得鎖定並且傳回true,如果目前的鎖無法取得到,那麼這個方法會立刻回傳falsetryLcok(long time,TimeUnit unit)在指定時間內嘗試取得鎖如果可以取得鎖,那麼取得鎖定並且傳回true,如果目前的鎖定無法獲取,那麼目前的執行緒變得不可被調度,直到下面三件事之一發生:unlock釋放目前執行緒佔用的鎖定newCondition傳回一個與目前的鎖定關聯的條件變數。在使用這個條件變數之前,當前執行緒必須佔用鎖。呼叫Condition的await方法,會在等待之前原子地釋放鎖定,並在等待被喚醒後原子的獲取鎖定

接下來,我們將圍繞著lock和unlock這兩個方法,來介紹整個ReentrantLock是怎麼運作的。在介紹ReentrantLock之前,我們先來看看ReentrantLock的類別層次結構以及和它密切相關的AbstractQueuedSynchronizer

1.2、ReentrantLock類別層次結構

##ReentrantLockrantLock類別層次結構

##ReentrantLock實作了Lock接口,內部有三個內部類,Sync、NonfairSync、FairSync,Sync是一個抽象類型,它繼承AbstractQueuedSynchronizer,這個AbstractQueuedSynchronizer是一個模板類,它實現了許多和鎖相關的功能,並提供了鉤子方法使用者實現,如tryAcquire,tryRelease等。 Sync實作了AbstractQueuedSynchronizer的tryRelease方法。 NonfairSync和FairSync兩個類別繼承自Sync,實作了lock方法,然後分別公平搶佔和非公平搶佔針對tryAcquire有不同的實作。

1.3、AbstractQueuedSynchronizer

首先,AbstractQueuedSynchronizer繼承自AbstractOwnableSynchronizer,AbstractOwnableSynchronizer的實作很簡單,它表示獨佔的同步器,內部使用變數exclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusiveOwnerThreadexclusive 的執行緒表示獨佔的線程。

其次,AbstractQueuedSynchronizer內部使用CLH鎖定隊列來將並發執行變成串行執行。整個隊列是一個雙向鍊錶。每個CLH鎖定佇列的節點,會保存前一個節點和後一個節點的引用,目前節點對應的線程,以及一個狀態。這個狀態用來表示該線程是否應該block。當節點的前一個節點被釋放的時候,當前節點就被喚醒,成為頭部。新加入的節點會放在佇列尾部。

二、 非公平鎖定的lock方法

2.1、lock方法流程圖

2.2、lock方法詳細描述

#1、在初始化ReentrantLock的時候,如果我們不傳參數是否公平,那麼預設使用非公平鎖,也就是NonfairSync。

2、當我們呼叫ReentrantLock的lock方法的時候,其實是呼叫了NonfairSync的lock方法,這個方法先用CAS操作,去嘗試搶佔該鎖。如果成功,就把目前執行緒設定在這個鎖上,表示搶佔成功。如果失敗,則呼叫acquire模板方法,等待搶佔。程式碼如下:

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
}

3、呼叫acquire(1)實際上使用的是AbstractQueuedSynchronizer的acquire方法,它是一套鎖搶佔的模板,總體原理是先去嘗試獲取鎖,如果沒有獲取成功,就在CLH隊列中增加一個目前執行緒的節點,表示等待搶佔。然後進入CLH佇列的搶佔模式,進入的時候也會去執行一次取得鎖的操作,如果還是取得不到,就呼叫LockSupport.park將目前執行緒掛起。那麼當前執行緒什麼時候會被喚醒呢?當持有鎖的那個執行緒呼叫unlock的時候,會將CLH佇列的頭節點的下一個節點上的執行緒喚醒,呼叫的是LockSupport.unpark方法。 acquire程式碼比較簡單,具體如下:

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

3.1、acquire方法內部先使用tryAcquire這個鉤子方法去嘗試再次取得鎖,這個方法在NonfairSync這個類別中其實就是使用了nonfairTryAcquire,具體實作原理是先比較當前鎖的狀態是否為0,如果是0,則嘗試去原子搶佔這個鎖(設定狀態為1,然後把當前執行緒設定成獨佔執行緒),如果目前鎖的狀態不是0,就去比較目前執行緒和佔用鎖的線程是不是一個線程,如果是,會去增加狀態變數的值,從這裡看出可重入鎖之所以可重入,就是同一個線程可以反覆使用它佔用的鎖。如果以上兩種情況都不通過,則傳回失敗false。程式碼如下:

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

3.2、tryAcquire一旦回傳false,就會則進入acquireQueued流程,也就是基於CLH佇列的搶佔模式:

3.2.1、首先,在CLH鎖定佇列尾部增加一個等待節點,這個節點保存了當前線程,透過呼叫addWaiter實現,這裡需要考慮初始化的情況,在第一個等待節點進入的時候,需要初始化一個頭節點然後把當前節點加入到尾部,後續則直接在尾部加入節點就行了。

程式碼如下:

private Node addWaiter(Node mode) {
		// 初始化一个节点,这个节点保存当前线程
        Node node = new Node(Thread.currentThread(), mode);
        // 当CLH队列不为空的视乎,直接在队列尾部插入一个节点
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
		// 当CLH队列为空的时候,调用enq方法初始化队列
        enq(node);
        return node;
}

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 初始化节点,头尾都指向一个空节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {// 考虑并发初始化
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}
3.2.2、將節點增加到CLH佇列後,進入acquireQueued方法。

首先,外層是一個無限###for循環###,如果當前節點是頭節點的下個節點,並且透過tryAcquire取得了鎖,說明頭節點已經釋放了鎖,當前線程是被頭節點那個執行緒喚醒的,這時候就可以將目前節點設定成頭節點,並且將failed標記設為false,然後再回傳。至於上一個節點,它的next變數被設定為null,下次GC的時候會被清理掉。 ###

如果本次循环没有获取到锁,就进入线程挂起阶段,也就是shouldParkAfterFailedAcquire这个方法。

代码如下:

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

3.2.3、如果尝试获取锁失败,就会进入shouldParkAfterFailedAcquire方法,会判断当前线程是否挂起,如果前一个节点已经是SIGNAL状态,则当前线程需要挂起。如果前一个节点是取消状态,则需要将取消节点从队列移除。如果前一个节点状态是其他状态,则尝试设置成SIGNAL状态,并返回不需要挂起,从而进行第二次抢占。完成上面的事后进入挂起阶段。

代码如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        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;
    }

3.2.4、当进入挂起阶段,会进入parkAndCheckInterrupt方法,则会调用LockSupport.park(this)将当前线程挂起。代码:

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

三、 非公平锁的unlock方法

3.1、unlock方法的活动图

3.2、unlock方法详细描述

1、调用unlock方法,其实是直接调用AbstractQueuedSynchronizer的release操作。

2、进入release方法,内部先尝试tryRelease操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。

3、一旦tryRelease成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。

4、一旦下一个节点的线程被唤醒,被唤醒的线程就会进入acquireQueued代码流程中,去获取锁。

具体代码如下:

unlock代码:

public void unlock() {
        sync.release(1);
}

release方法代码:

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

Sync中通用的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;
 }

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

四、 公平锁和非公平锁的区别

公平锁和非公平锁,在CHL队列抢占模式上都是一致的,也就是在进入acquireQueued这个方法之后都一样,它们的区别在初次抢占上有区别,也就是tryAcquire上的区别,下面是两者内部调用关系的简图:

NonfairSync
lock —> compareAndSetState
                | —> setExclusiveOwnerThread
      —> accquire
		     | —> tryAcquire
                           |—>nonfairTryAcquire
                |—> acquireQueued

FairSync
lock —> acquire
               | —> tryAcquire
                           |—>!hasQueuePredecessors
                           |—>compareAndSetState
                           |—>setExclusiveOwnerThread
               |—> acquireQueued

真正的区别就是公平锁多了hasQueuePredecessors这个方法,这个方法用于判断CHL队列中是否有节点,对于公平锁,如果CHL队列有节点,则新进入竞争的线程一定要在CHL上排队,而非公平锁则是无视CHL队列中的节点,直接进行竞争抢占,这就有可能导致CHL队列上的节点永远获取不到锁,这就是非公平锁之所以不公平的原因。

五、 总结

线程使用ReentrantLock获取锁分为两个阶段,第一个阶段是初次竞争,第二个阶段是基于CHL队列的竞争。在初次竞争的时候是否考虑队列节点直接区分出了公平锁和非公平锁。在基于CHL队列的锁竞争中,依靠CAS操作保证原子操作,依靠LockSupport来做线程的挂起和唤醒,使用队列来保证并发执行变成了串行执行,从而消除了并发所带来的问题。总体来说,ReentrantLock是一个比较轻量级的锁,而且使用面向对象的思想去实现了锁的功能,比原来的synchronized关键字更加好理解。

方法名稱 描述
lock 取得鎖,如果鎖定無法取得,那麼目前的執行緒就變成不可被調度,直到鎖定被取得到
1、當前線程獲取到了鎖

#2、其他的執行緒中斷了目前的執行緒

1、目前執行緒取得到了鎖定

2、當前執行緒被其他執行緒中斷

3、指定的等待時間到了

 

以上是詳解Java中可重入鎖ReentrantLock原理的範例程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn