逐漸深入Java多線程(四)----Java的ReentrantLock簡介

目錄

一,ReentrantLock簡介

二,ReentrantLock可重入的原理

三,非公平鎖的加鎖

四,公平鎖的加鎖

五,解鎖邏輯

六,ReentrantLock的Condition

七,ReentrantLock中的其他方法

八,關於Unsafe


一,ReentrantLock簡介

ReentrantLock,字面意思是可重入鎖,由JDK1.5加入,位於java.util.concurrent.locks包中。

ReentrantLock是一種可重入且互斥的鎖,和synchronized關鍵字效果差不多,但是功能更多樣,使用方式更豐富一些。

 

從ReentrantLock的構造方法中可以看到,ReentrantLock支持公平鎖和非公平鎖的模式:

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

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

默認是非公平鎖。

所謂公平鎖,就是保證先申請鎖的線程會先獲得鎖,用隊列實現。

非公平鎖就是先申請鎖的線程不一定會先獲得鎖,有可能會出現飢餓的情況,效率比公平高一些。

 

在ReentrantLock類中定義了抽象類Sync,並且維護了一個同名參數sync,可重入鎖對象很多操作都由sync參數來完成。

Sync類繼承了抽象類AbstractQueuedSynchronizer,AbstractQueuedSynchronizer維護了一個雙向鏈表隊列,設有head和tail節點。

AbstractQueuedSynchronizer類繼承了AbstractOwnableSynchronizer類,此類用來記錄鎖的歸屬線程。

類的繼承關係如下圖:

抽象類Sync有兩個默認的實現類(子類),都是ReentrantLock類中定義的內部類:

FairSync,公平鎖的實現類

NonfairSync,非公平鎖的實現類

從ReentrantLock的構造方法可以看到,無參構造使用的是非公平鎖NonfairSync。

 

二,ReentrantLock可重入的原理

加鎖:lock(),lockInterruptibly(),tryLock()等方法可以用來加鎖

  1. 在AbstractQueuedSynchronizer中維護了state參數,類似狀態的功能,state爲0時可以獲得鎖。
  2. 線程獲得鎖後,state加1,設置鎖的所屬線程爲當前線程。
  3. 同一線程試圖再次獲得鎖時,鎖的所屬線程就是自己,則可以獲得鎖(即可重入),state再加1。

解鎖:unlock()方法可以用來解鎖

  1. state減1。
  2. state減到0時,說明該線程已經完全放棄了該鎖,設置鎖的所屬線程爲null。

FairSync和NonfairSync對於加鎖和解鎖有不同的實現方法,下面分別看一下。

 

三,非公平鎖的加鎖

以lock()方法爲例:

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

調用NonfairSync的lock()方法:

final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

首先調用的是compareAndSetState(0, 1)方法,這個方法在AbstractQueuedSynchronizer中定義:

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

實際上是用UNSAFE把state從0改爲1,其實就是搶鎖,改成功了就算是獲得了鎖,這是一個CAS操作。

此處是第一個非公平鎖體現不公平性的地方:線程搶鎖時沒有考慮隊列中有沒有正在等待的線程,鑑於解鎖時會先把state改成0,再喚醒隊列中的線程,此時就很有可能會被某個還沒加入隊列的線程在這一步搶鎖成功。

如果compareAndSetState(0, 1)調用成功,說明獲取到了鎖,下面調用setExclusiveOwnerThread()方法把歸屬線程設爲自己:

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

此方法在AbstractOwnableSynchronizer類中,實際上這個類就維護了這麼一個屬性以及他的get/set方法:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** Use serial ID even though all fields transient. */
    private static final long serialVersionUID = 3737899427754241961L;

    /**
     * Empty constructor for use by subclasses.
     */
    protected AbstractOwnableSynchronizer() { }

    /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * Sets the thread that currently owns exclusive access.
     * A {@code null} argument indicates that no thread owns access.
     * This method does not otherwise impose any synchronization or
     * {@code volatile} field accesses.
     * @param thread the owner thread
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    /**
     * Returns the thread last set by {@code setExclusiveOwnerThread},
     * or {@code null} if never set.  This method does not otherwise
     * impose any synchronization or {@code volatile} field accesses.
     * @return the owner thread
     */
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

如果前面的compareAndSetState(0, 1)調用失敗,即沒獲取到鎖,則調用acquire(1)方法:

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

此方法在AbstractQueuedSynchronizer中,首先調用tryAcquire()方法進行一次不公平競爭鎖,方法定義在NonfairSync中:

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

下面是nonfairTryAcquire()方法:

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 exceed
        setState(nextc);
        return true;
    }
    return false;
}

代碼中可以看到,首先獲取狀態state,然後分幾種情況:

1,state=0。則立即調用compareAndSetState()方法試圖獲得鎖,成功則調用setExclusiveOwnerThread()方法設置歸屬線程,返回true,此段邏輯和lock()方法中基本相同。

2,歸屬線程就是自己。把state加1,返回true。

3,其他情況,表示獲取鎖失敗,返回false。

 

於是我們看到了第二處非公平鎖的不公平性:此代碼邏輯發生在第一次試圖搶鎖失敗之後,tryAcquire()方法中又一次試圖搶鎖。

 

此時acquire()方法中的tryAcquire()方法執行完成,顯然tryAcquire()方法返回false後就執行後面的acquireQueued()方法,acquireQueued()方法的一個參數是通過調用addWaiter(Node.EXCLUSIVE)來獲得的,先來看一下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;
}

方法的功能是把一個排他模式的節點加入隊列,創建一個新節點,如果tail爲空時則把該節點放到隊尾,tail.next指向該節點,否則調用enq()方法將節點入隊。

獲得了排他模式的節點後,下面看一下acquireQueued()方法方法的代碼:

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

在代碼邏輯中,p是新節點的前一節點,如果p是head節點,說明此新增節點是隊列中的第一個節點(head節點不在隊列中,head的next指向的是隊列的第一節點),於是直接調用tryAcquire()方法試圖得到鎖,如果成功則把head節點設爲此新增節點。如果p不是第一個節點,或者是第一節點但是搶鎖失敗,則調用parkAndCheckInterrupt()方法阻塞此節點代表的線程。

此邏輯中的搶鎖貌似不能算是不公平的,畢竟在這裏只有第一節點才能搶。

代碼會重複以上過程,直到成功獲取鎖爲止。

至此非公平鎖的加鎖邏輯結束。

 

從非公平鎖加鎖的邏輯中可以看到,新的線程試圖獲得鎖時,有兩次不公平的搶鎖機會,而且這種直接搶鎖的方式比在隊列中等待喚醒然後搶鎖的成功率更高,使得非公平鎖顯得更加的不公平。

雖然很不公平,但是這種搶鎖方式帶來了很大的性能提升:通過這種方式獲得鎖,線程就不用加入阻塞隊列了,生成隊列節點和入隊之類的操作就免了,入隊的CAS重試也免了(入隊競爭激烈的時候效果更佳),線程阻塞喚醒什麼的上下文切換也免了(線程加鎖時間短時效果更佳),於是總體性能會提高很多,不怕餓的可以用這種非公平的模式。

 

四,公平鎖的加鎖

以lock()方法爲例:

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

調用AbstractQueuedSynchronizer的acquire()方法,和非公平鎖一樣:

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

tryAcquire()方法定義在FairSync中,和NonfairSync的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()方法,這個方法用於判斷該節點前還有沒有其他節點,也就是說,state爲0時隊列前面還有線程在排隊的話,就不會試圖獲得鎖了,所謂公平鎖的公平性就是通過這個方法來實現的:

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    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());
}

 

五,解鎖邏輯

ReentrantLock的unlock()方法用來解鎖:

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

公平鎖和非公平鎖的release()方法都在AbstractQueuedSynchronizer類中:

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()方法,這個方法的具體實現在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);
    }
    setState(c);
    return free;
}

方法比較簡單,state減1,如果減到0就把歸屬線程設爲null,然後返回true。

至此解鎖邏輯結束。

 

六,ReentrantLock的Condition

ReentrantLock的Condition提供了一套新的Object監視器方法,這是一種線程通信的方式。

ReentrantLock的Condition通過以下代碼獲得:

final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
final Condition condition2 = lock.newCondition();

在Condition出現之前,我們使用的是Object的notify()、wait()、notifyAll()等方法,而Condition在粒度上更細化了,在一個Lock中可以創建多個Condition,每個Condition都可以有自己的等待隊列(wait-sets),便於不同的線程進行不同的處理。另外,從性能上來說Condition比Object版要高一些,提供的方法也更豐富一些。

Condition提供了await()方法實現Object的wait()功能,signal()方法實現Object的notify(),signalAll()方法實現Object的notifyAll()。另外Condition還提供了打斷,超時等方法。

使用Condition的signal(),await(),signalAll()方法前,需要先進行ReentrantLock的lock()操作。

Condition的這幾個方法不要和Object版的方法混用。

Condition非常適合生產者消費者模式。

 

用final標識的Condition對象可以參與線程間的通信,這也許就是這個類起名叫Condition(情景)的原因,被某個Condition對象的await()方法阻塞的線程A,當另一個線程B調用這個Condition對象的signal()方法時,線程A就會被喚醒,此時Condition對象就成了各線程間協調阻塞和喚醒的通用場景標識。

下面是一個Condition使用的例子,先讓線程A負責輸出1到3,然後喚醒線程B,線程B負責輸出4到6,然後喚醒線程A,A最後輸出over。代碼中特意讓線程B先於A開始執行。

下面是代碼:

static class ThisIsANumber {
    public int number = 1;
}

public static void main(String[] args) {
    final Lock lock = new ReentrantLock();
    final Condition condition = lock.newCondition();
    final ThisIsANumber num = new ThisIsANumber();

    //線程A,負責輸出1到3,輸出完就喚醒其他線程,等其他線程把數字變爲6以上時,輸出over
    Thread threadA = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                lock.lock();
                System.out.println("threadA get lock");
                while (num.number <= 3) {       //數字小於3則輸出到3
                    System.out.println(num.number);
                    num.number++;
                }
                System.out.println("threadA call signal");
                condition.signal();     //喚醒其他線程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("threadA unlock");
                lock.unlock();
            }

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            try {
                lock.lock();
                System.out.println("threadA get lock again");
                if (num.number <= 6) {      //數字小於6則等待
                    System.out.println("threadA await() again");
                    condition.await();
                    System.out.println("threadA await() over");
                }
                System.out.println("threadA over");     //線程完成
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("threadA unlock again");
                lock.unlock();
            }
        }
    });

    //線程B,負責輸出3到6,輸出完則喚醒其他線程
    Thread threadB = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                lock.lock();
                System.out.println("threadB get lock");
                if (num.number <= 3) {      //數字小於3則等待
                    System.out.println("threadB await()");
                    condition.await();
                    System.out.println("threadB await() over");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("threadB unlock");
                lock.unlock();
            }
            
            try {
                lock.lock();
                System.out.println("threadB get lock again");
                while (num.number <= 6) {       //數字小於6則輸出到6
                    System.out.println(num.number);
                    num.number++;
                }
                System.out.println("threadB call signal");
                condition.signal();     //喚醒其他線程
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("threadB unlock again");
                lock.unlock();
            }
        }

    });

    threadB.start();
    threadA.start();
}

輸出的結果是這樣的:

threadB get lock
threadB await()
threadA get lock
1
2
3
threadA call signal
threadA unlock
threadB await() over
threadB unlock
threadB get lock again
4
5
6
threadB call signal
threadB unlock again
threadA get lock again
threadA over
threadA unlock again

下面逐條解釋一下這個輸出的結果:

  • threadB get lock,由於先啓動的線程B,所以B先獲得鎖。
  • threadB await(),number小於3,線程B開始WAIT。
  • threadA get lock,線程B開始WAIT後,線程A立即獲得了鎖,注意此時線程B還沒有顯式調用unlock()方法。
  • 1,線程A輸出
  • 2,線程A輸出
  • 3,線程A輸出
  • threadA call signal,線程A輸出完成後喚醒其他線程,也就是線程B。
  • threadA unlock,線程A喚醒線程B後等待了1秒,但是線程B一直沒能開始執行,直到線程A調用unlock()方法。
  • threadB await() over,線程A調用unlock()方法釋放鎖後,線程B才獲得鎖,並從WAIT狀態變爲RUNNING狀態。
  • threadB unlock,線程B釋放鎖。
  • threadB get lock again,線程B再次獲得鎖,因爲此時線程A釋放鎖後Sleep了2秒,否則此時應該是線程A和線程B同時搶鎖,而且線程A成功獲得鎖的概率要比線程B大的多。
  • 4,線程B輸出
  • 5,線程B輸出
  • 6,線程B輸出
  • threadB call signal,線程B輸出完畢,喚醒線程A。
  • threadB unlock again,線程B喚醒線程A後等待了1秒,但是線程A在此期間也沒能開始執行,直到線程B釋放鎖。
  • threadA get lock again,線程A獲得鎖。
  • threadA over,線程A輸出over。
  • threadA unlock again,線程A釋放鎖。

 

注意到中間有一次兩線程搶鎖,因爲線程A當時在Sleep所以線程B搶到了鎖,而且我們發現如果當時線程A沒有在Sleep(也就是線程A釋放鎖後不Sleep2秒),那麼線程A獲得鎖的可能性非常大,這種情況下的輸出的結果就是這樣了:

threadB get lock
threadB await()
threadA get lock
1
2
3
threadA call signal
threadA unlock
threadA get lock again
threadA await() again
threadB await() over
threadB unlock
threadB get lock again
4
5
6
threadB call signal
threadB unlock again
threadA await() over
threadA over
threadA unlock again

關於Condition我們有以下需要注意:

1,使用Condition的await()或signal()方法時,需要事先獲得鎖,否則將會拋異常。

2,線程使用await()方法,自己進入WAIT狀態,會直接讓出鎖,不需要調用unlock()方法,其他線程就可以開始搶鎖了。

3,線程調用signal()方法,喚醒其他線程時,被喚醒的線程無法立即被喚醒。被喚醒的線程還需要拿到鎖才能從等待狀態變成RUNNING狀態。持鎖線程調用unlock()或await()方法都會釋放鎖。

4,sleep()方法不會使線程讓出鎖,這一點和await()方法不一樣。

 

關於生產者消費者模式需要注意:

使用生產者消費者模式時,特別是多個生產者和多個消費者的模式下,最好讓生產者和消費者分別使用不同的Condition對象,因爲Condition的signal()方法喚醒的不一定是哪個線程,所以可能會出現生產者處理完成後又喚醒一個生產者,或者消費者處理完成後又喚醒一個消費者,流程會卡住,所以最好使用兩個Condition對象,讓生產者喚醒的一定是消費者,消費者喚醒的也一定是生產者。

 

七,ReentrantLock中的其他方法

1, void lockInterruptibly()

這個方法也是用於獲取鎖,不過在等鎖的過程中如果被interrupt,線程會立即拋出InterruptedException異常,不再繼續等鎖。相比之下,lock()方法在等鎖期間被interrupt不會立即響應,而是會等到搶到鎖,然後再interrupt。

2,boolean tryLock()

嘗試搶鎖,搶鎖成功則返回true,失敗則返回false,立即返回結果,不等待。

3,boolean isHeldByCurrentThread()

判斷鎖是否是當前線程持有。

4,boolean isLocked()

判斷鎖是否被任何線程持有,官方建議此方法用於鎖狀態監控,不要用於同步控制。

5,boolean isFair()

判斷鎖是否是公平鎖。公平鎖則返回true,非公平鎖返回false。

6, Thread getOwner()

返回持鎖的線程。沒有線程持鎖則返回null。

另外,官方註釋中提到,如果調用這個方法的線程不是鎖的持有者,返回的線程能體現鎖的近似狀態,比如有很多線程正在搶鎖(暫時還沒有線程搶到鎖),這個方法的返回值就可能是null。沒明白這段註釋是想說什麼。

7,boolean hasQueuedThreads()

判斷是否有線程正在等待搶鎖。

官方註釋中提到,不要用這個方法來判斷是否有線程會去搶鎖,因爲取消操作隨時都會發生,這個方法是用來監控鎖狀態的。

8,boolean hasQueuedThread(Thread thread)

判斷指定線程是否正在等待搶鎖。

和hasQueuedThreads()方法一樣,官方建議不要用這個方法來判斷指定線程會不會去搶鎖。

9,int getQueueLength()

返回在隊列中等待搶鎖的線程數量近似值。所謂近似值指的是,隊列中的線程數量是動態變化的,該方法獲取數量需要遍歷等待隊列,在遍歷期間線程數量都有可能變化,所以只能是近似值。所以,不要用這個方法進行同步校驗,只能用來監控鎖狀態。

10,Collection<Thread> getQueuedThreads()

返回在隊列中等待搶鎖的線程列表。返回的列表元素沒有特定順序。此返回值是不精確的,因爲隊列中的線程隨時會變。這個方法主要是用來給子類重寫用的。

11,boolean hasWaiters(Condition condition)

判斷某個Condition下有沒有等待狀態的線程。這個方法的返回值不代表將來一定會喚醒那麼多線程,因爲這些線程隨時可能會被interrupt或超時。此方法只用來監控系統狀態。

12,int getWaitQueueLength(Condition condition)

返回在指定Condition下正在等待的線程數。不能用來做同步控制,只用來監控系統狀態。

13,Collection<Thread> getWaitingThreads(Condition condition)

返回指定Condition下正在等待的線程列表。返回的列表元素沒有特定順序。返回結果不精確,因爲這些線程隨時可能會被interrupt或超時。這個方法主要是用來給子類重寫用的。

 

八,關於Unsafe

Unsafe是sun.misc包下的類,提供了很多native標識的方法,是一些很底層的方法,甚至會繞過JVM。

在ReentrantLock中很多lock操作或者CAS操作都是由Unsafe類完成的。

官方不建議開發者使用Unsafe類,一個原因是sun.misc下的類本來是給Java自己用的,Java完全有可能在以後的版本中移除裏面的類,包括Unsafe。二是因爲Unsafe權限太高,提供的硬件級別操作會繞開JVM管理,自己操作內存地址,修改變量,操作線程,要是玩脫了JVM可能會崩,或者出現內存泄露什麼的。正如他的名字所表達的意思一樣,這個類確實不太安全。

 

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