Java併發編程之鎖機制之(ReentrantLock)重入鎖 原

ReentrantLock基本介紹

ReentrantLock 是一種 可重入 的 互斥鎖 ,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖相同的一些基本行爲和語義,但功能更強大。

ReentrantLock 將由最近成功獲得鎖,並且還沒有釋放該鎖的線程所擁有。當鎖沒有被另一個線程所擁有時,調用 lock 的線程將成功獲取該鎖並返回。如果當前線程已經擁有該鎖,此方法將立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法來檢查此情況是否發生。

此類的構造方法接受一個可選的 公平 參數。當設置爲 true 時(也是當前 ReentrantLock爲公平鎖的情況 ),在多個線程的爭用下,這些鎖傾向於將訪問權授予等待時間最長的線程。否則此鎖將無法保證任何特定訪問順序。與採用默認設置(使用不公平鎖)相比,使用公平鎖的程序在許多線程訪問時表現爲很低的總體吞吐量(即速度很慢,常常極其慢),但是在獲得鎖和保證鎖分配的均衡性時差異較小。不過要注意的是,公平鎖不能保證線程調度的公平性。因此,使用公平鎖的衆多線程中的一員可能獲得多倍的成功機會,這種情況發生在其他活動線程沒有被處理並且目前並未持有鎖時。還要注意的是,未定時的 tryLock 方法並沒有使用公平設置。因爲即使其他線程正在等待,只要該鎖是可用的,此方法就可以獲得成功。

ReentrantLock 類基本結構

通過上文的簡單介紹後,我相信很多小夥伴還是一臉懵逼,只知道上文我們提到了 ReentrantLock 與 synchronized 相比有相同的語義,同時其內部分爲了 公平鎖 與 非公平鎖 兩種鎖的類型,且該鎖是支持 重進入 的。那麼爲了方便大家理解這些知識點,我們先從其類的基本結構講起。具體類結構如下圖所示:

從上圖中我們可以看出,在 ReentrantLock 類中,定義了三個靜態內部類, Sync 、 FairSync(公平鎖) 、 NonfairSync(非公平鎖 )。其中 Sync 繼承了 AQS(AbstractQueuedSynchronizer) ,而 FairSync 與 NonfairSync 又分別繼承了 Sync 。關於 ReentrantLock 基本類結構如下所示:

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;

	//默認無參構造函數,默認爲非公平鎖
    public ReentrantLock() {
        sync = new NonfairSync();
    }
	//帶參數的構造函數,用戶自己來決定是公平鎖還是非公平鎖
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    //抽象基類繼承AQS,公平鎖與非公平鎖繼承該類,並分別實現其lock()方法
    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();
        //省略部分代碼..
    }
    
	//非公平鎖實現
    static final class NonfairSync extends Sync {...}
    
    //公平鎖實現
    static final class FairSync extends Sync {....}
   
    //鎖實習,根據具體子類實現調用
    public void lock() {
        sync.lock();
    }
	//響應中斷的獲取鎖
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
	//嘗試獲取鎖,默認採用非公平鎖方法實現
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
	//超時獲取鎖
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
	//釋放鎖
    public void unlock() {
        sync.release(1);
    }
    //創建鎖條件(從Condetion來理解,就是創建等待隊列)
    public Condition newCondition() {
        return sync.newCondition();
    }
    //省略部分代碼....
}
複製代碼

這裏爲了方便大家理解 ReentrantLock 類的整體結構,我省略了一些代碼及重新排列了一些代碼的順序。

從代碼中我們可以看出。整個 ReentrantLock 類的實現其實都是交給了其內部 FairSync 與 NonfairSync 兩個類。在 ReentrantLock 類中有兩個構造函數,其中不帶參數的構造函數中默認使用的 NonfairSync(非公平鎖) 。另一個帶參數的構造函數,用戶自己來決定是 FairSync(公平鎖) 還是非公平鎖。

重進入實現

在上文中,我們提到了 ReentrantLock 是支持重進入的,那什麼是重進入呢? 重進入是指任意線程在獲取到鎖之後能夠再次獲取該鎖,而不會被鎖阻塞 。那接下來我們看看這個例子,如下所示:

class ReentrantLockDemo {
    private static final ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                methodA();
            }
        });
        thread.start();
    }
    
    public static void methodA() {
        lock.lock();
        try {
            System.out.println("我已經進入methodA方法了");
            methodB();//方法A中繼續調用方法B
        } finally {
            lock.unlock();
        }
    }

    public static void methodB() {
        lock.lock();
        try {
            System.out.println("我已經進入methodB方法了");
        } finally {
            lock.unlock();
        }
    }
}
//輸出結果
我已經進入methodA方法了
我已經進入methodB方法了
複製代碼

在上述代碼中我們聲明瞭一個線程調用methodA()方法。同時在該方法內部我們又調用了methodB()方法。從實際的代碼運行結果來看,當前線程進入方法A之後。在方法B中再次調用 lock.lock(); 時,該線程並沒有被阻塞。也就是說 ReentrantLock 是支持重進入的。那下面我們就一起來看看其內部的實現原理。

因爲 ReenTrantLock 將具體實現交給了 NonfairSync(非公平鎖) 與 FairSync(公平鎖) 。同時又因爲上述提到的兩個鎖,關於重進入的實現又非常相似。所以這裏將採用 NonfairSync(非公平鎖) 的重進入的實現,來進行分析。希望讀者朋友們閱讀到這裏的時候需要注意,不是我懶哦,是真的很相似哦。

好了下面我們來看代碼。關於NonfairSync代碼如下所示:

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);
        }
    }
複製代碼

當我們調用lock()方法時,通過CAS操作將AQS中的state的狀態設置爲1,如果成功,那麼表示獲取同步狀態成功。那麼會接着調用 setExclusiveOwnerThread(Thread thread) 方法來設置當前佔有鎖的線程。如果失敗,則調用 acquire(int arg) 方法來獲取同步狀態(該方法是屬於AQS中的獨佔式獲取同步狀態的方法,對該方法不熟悉的小夥伴,建議閱讀 Java併發編程之鎖機制之AQS(AbstractQueuedSynchronizer) )。而該方法內部會調用 tryAcquire(int acquires) 來嘗試獲取同步狀態。通過觀察,我們發現最終會調用 Sync 類中的 nonfairTryAcquire(int acquires) 方法。我們繼續跟蹤。

final boolean nonfairTryAcquire(int acquires) {
		    //獲取當前線程
            final Thread current = Thread.currentThread();
            int c = getState();
            //(1)判斷同步狀態,如果未設置,則設置同步狀態
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //(2)如果當前線程已經獲取了同步狀態,則增加同步狀態的值。
            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)先判斷同步狀態, 如果未曾設置,則設置同步狀態,並設置當前佔有鎖的線程。
  • (2)判斷是否是同一線程,如果當前線程已經獲取了同步狀態(也就是獲取了鎖),那麼增加同步狀態的值。

也就是說,如果同一個鎖獲取了鎖N( N爲正整數 )次,那麼對應的同步狀態 (state) 也就等於N。那麼接下來的問題來了, 如果當前線程重複N次獲取了鎖,那麼該線程是否需要釋放鎖N次呢? 答案當然是必須的。當我們調用 ReenTrantLock 的unlock()方法來釋放同步狀態(也就是釋放鎖)時,內部會調用 sync.release(1); 。最終會調用 Sync 類的 tryRelease(int releases) 方法。具體代碼如下所示:

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;
        }
複製代碼

從代碼中,我們可以知道,每調用一次 unlock() 方法會將當前同步狀態減一。也就是說如果當前線程獲取了鎖N次,那麼獲取鎖的相應線程也需要調用 unlock() 方法N次。這也是爲什麼我們在之前的重入鎖例子中,爲什麼 methodB 方法中也要釋放鎖的原因。

非公平鎖

在ReentrantLock中有着 非公平鎖 與 公平鎖 的概念,這裏我先簡單的介紹一下 公平 這兩個字的含義。 這裏的公平是指線程獲取鎖的順序。也就是說鎖的獲取順序是按照當前線程請求的絕對時間順序,當然前提條件下是該線程獲取鎖成功 。

那麼接下來,我們來分析在ReentrantLock中的非公平鎖的具體實現。

這裏需要大傢俱備 AQS(AbstractQueuedSynchronizer) 類的相關知識。如果大家不熟悉這塊的知識。建議大家閱讀 Java併發編程之鎖機制之AQS(AbstractQueuedSynchronizer) 。

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

        final void lock() {
            if (compareAndSetState(0, 1))//直接獲取同步狀態成功,那麼就不再走嘗試獲取鎖的過程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        //省略部分代碼...
    }
複製代碼

當在ReentrantLock在 非公平鎖的模式下 ,去調用lock()方法。那麼接下來最終會走 AQS(AbstractQueuedSynchronizer) 下的 acquire(int arg)(獨佔式的獲取同步狀態) ,也就是如下代碼:

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

那麼結合之前我們所講的AQS知識,在多個線程在 獨佔式 請求共享狀態下(也就是請求鎖)的情況下,在AQS中的同步隊列中的線程節點情況如下圖所示:

那麼我們試想一種情況,當Nod1中的線程執行完相應任務後,釋放鎖後。這個時候本來該喚醒當前線程節點的 下一個節點 ,也就是 Node2中的線程 。這個時候突然另一線程突然來獲取線程(這裏我們用節點 Node5 來表示)。具體情況如下圖所示:

那麼根據AQS中獨佔式獲取同步狀態的邏輯。只要 Node5對應的線程獲取同步狀態成功 。那麼就會出現下面的這種情況,具體情況如下圖所示:在此我向大家推薦一個架構學習交流裙。交流學習裙號:821169538,裏面會分享一些資深架構師錄製的視頻錄像 

從上圖中我們可以看出,由於Node5對象的線程搶佔了獲取同步狀態(獲取鎖)的機會,本身應該被喚醒的 Node2 線程節點。因爲獲取同步狀態失敗。所以只有再次的陷入阻塞。那麼綜上。我們可以知道。 非公平鎖獲取同步狀態(獲取鎖)時不會考慮同步隊列中中等待的問題。會直接嘗試獲取鎖。也就是會存在後申請,但是會先獲得同步狀態(獲取鎖)的情況。

公平鎖

理解了非公平鎖,再來理解公平鎖就非常簡單了。下面我們來看一下公平鎖與非公平鎖的加鎖的源碼:

從源碼我們可以看出,非公平鎖與公平鎖之間的代碼唯一區別就是多了一個判斷條件 !hasQueuedPredecessors()(圖中紅框所示) 。那我們查看其源碼(該代碼在AQS中,強烈建議閱讀 Java併發編程之鎖機制之AQS(AbstractQueuedSynchronizer)

public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
複製代碼

代碼理解理解起來非常簡單,就是判斷當前當前head節點的next節點是不是當前請求同步狀態(請求鎖)的線程。也就是語句 ((s = h.next) == null || s.thread != Thread.currentThread() 。那麼接下來結合AQS中的同步隊列我們可以得到下圖:

那麼綜上我們可以得出,公平鎖保證了線程請求的同步狀態(請求鎖)的順序。不會出現另一個線程搶佔的情況。

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