JUC-Lock工具解析

一、簡介

在進行多線程編程的時候,考慮的最多的問題之一就是併發的問題,也就是多線程對於共享變量的操作問題。許多時候我們會藉助於鎖這個工具來控制多線程併發對於共享變量的操作,是一種犧牲時間的併發保障機制。

在java中關於同步工具,有自帶的關鍵字synchronized,也有juc下利用AQS同步框架封裝的Lock工具,以及一些以AQS及其Lock爲基礎的同步工具CountDownLatch、CycleBarrier等,關於AQS的理解,可以在《JUC-AQS框架解析》文章中進行源碼級別的理解分析,本文章是在瞭解AQS基礎之上,來分析Lock工具的源碼邏輯執行過程。

二、概況

juc(java.util.concurrent)下locks包下封裝了我們經常使用的併發工具Lock,上面文章介紹的AQS也是在此包目錄下,在Lock以及ReadWriteLock接口下分別實現了普通的鎖和按照讀寫功能實現的讀寫鎖,首先我們看下Lock系列工具的關係:

 可以看到Lock和ReadWriteLock分別是兩個頂級的接口,ReentrantLock和ReentrantReadWriteLock分別是兩個接口的實現類,前者是我們在併發中經常使用的普通可重入鎖,後者則是讀寫鎖,意在提供讀讀不衝突的鎖類型,滿足不同的操作鎖。

三、解析

首先我們看下Lock接口定義信息,源碼如下:

    public interface Lock {
        //獲取鎖
        void lock();
        //響應中斷的獲取鎖
        void lockInterruptibly() throws InterruptedException;
        //嘗試加鎖,返回是否加鎖成功
        boolean tryLock();
        //一段時間內嘗試加鎖
        boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
        //釋放鎖
        void unlock();
        //鎖條件
        Condition newCondition();
    }

可以看到Lock接口大致定義了獲取鎖和釋放鎖的幾個方法,下面我們看下ReentrantLock的具體邏輯,首先我們從ReentrantLock的加鎖方法進入,也就是lock()方法,源碼如下:

    public void lock() {
        sync.lock();
    }

直接掉用了內部類Sync的lock()方法,我們看下Sync的lock()方法,源碼如下:

    abstract void lock();

Sync作爲ReentrantLock的內部類,繼承了AQS,也就是前面文章提到的AbstractQueuedSynchronizer框架,並且lock()作爲抽象方法,他的子類都要去重寫,因爲ReentrantLock支持公平和非公平鎖,依據傳入的參數決定。Sync分別有兩個子類,實現了fair和unfair鎖方式,默認的方式是非公平鎖,因此我們首先看下非公平鎖的sync子類加鎖方式,源碼如下:

    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);
        }
        //重寫AQS的tryAcquire()方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

可以到過程比較簡單,首先cas去獲取鎖,如果成功,則設置當前線程爲獨佔鎖線程;如果cas失敗則調用acquire()方法,此方式是我們在上篇《JUC-AQS框架解析》文章中講解到的,他是獨佔鎖的頂層入口,不理解的可以在去查看下,頂層入口獲取鎖的方法邏輯會調用自定義同步器也就是ReentrantLock的內部類Sync及其他的兩個子類重寫的獲取鎖方法,也是上面代碼片段的tryAcquire()方法,我們可以看到他調用的是父類Sync的nonfairTryAcquire()方法,下面我們看下父類Sync的nonfairTryAcquire()方法源碼結構:

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//未加鎖,則cas獲取鎖
                if (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;
        }

首先判斷共享資源是否是空閒狀態,如果是則cas去獲取鎖,成功則設置佔用鎖的線程,如果失敗則返回false;如果資源被佔用,則判斷是否是本線程佔用,也就是可重入鎖的概念,如果是則累加共享變量,如果不是則返回false。

當獲取鎖失敗的時候,則就進入了AQS的後續入隊等待操作接口,頂層AQS的acquire()方法邏輯如下:

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

以上是ReentrantLock非公平鎖方式獲取鎖的邏輯,下面我們看下鎖釋放鎖的邏輯:

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

可以看到掉用了Sync的父類AQS的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;
    }

然後按照我們之前講述的,則會調用自定義同步器重寫的tryRelease()方法,在ReentrantLock的Sync父類中:

    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);//鎖釋放,佔用線程爲null
        }
        setState(c);
        return free;
    }

可以看到釋放鎖的過程也是比較簡單的,首先校驗一下當前線程是否是佔有鎖的狀態,不是則直接拋出異常,如果釋放資源後,是無鎖狀態(state=0)則設置佔用鎖的線程標識爲null,然後設置state,返回free。state爲0,則free爲true,則喚醒後續線程去獲取共享資源,如果state不是0,則free爲false,則證明本線程重入鎖,未釋放完畢,不喚醒後續節點線程。

下面我們看下公平鎖模式下獲取鎖的過程,首先我們看下公平鎖的sync子類的lock()邏輯:

    final void lock() {
        acquire(1);
    }

可以看到,和非公平鎖邏輯不同之處在於,非公平鎖是先cas獲取鎖,看能否成功,而公平鎖則是不可以直接獲取鎖,而是直接入隊等待獲取鎖,下面我們看下公平鎖重寫的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()方法的邏輯,判斷是否有前節點的方法,邏輯如下:

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

返回false,也就是可以嘗試去獲取鎖的情況有:

1.h==t:頭尾節點相同,可以都是null,或者目前隊列只有一個節點;

2.h!=t,h.next() !=null,則看當前線程是不是頭節點的下一個節點線程,是則返回false;

回過頭來,看下公平鎖的加鎖過程,首先直接執行入隊的獲取鎖方法,只有在最前面的節點線程菜有資額獲取鎖。公平和非公平的含義在此實現,新的線程到來如果共享資源可用,則公平鎖還要判斷本線程是否是最前面的節點線程,否則無資格獲取鎖。

以上就是lock公平鎖以及非公平鎖的獲取、釋放鎖邏輯,ReentrantLock內部只是定義了獲取和釋放鎖的邏輯,體現在tryAcquire()和tryRelease()方法的邏輯,上層的等待入隊操作等,則是AQS內部已經實現好的功能。

剩下的一些方法包括響應中斷的獲取鎖的方法、直接返回是否獲取鎖的方法以及嘗試傳入時間內獲取鎖的方法,邏輯都很容易理解,可以自行去查看理解。

四、資源地址

官網:http://www.java.com

文檔:《Thinking in java》

jdk1.8版本源碼

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