Java 併發編程——ReentrantLock

一、簡介

ReentrantLock 是一個可重入獨佔式的鎖,相較於傳統的 Synchronized,它增加了輪詢、超時、中斷等高級功能。其類圖如下:

ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖,相比於 synchronized,它多了以下高級功能:

1. 等待可中斷

  當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改爲處理其他事情。

2. 可實現公平鎖

  公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。

  synchronized 中的鎖是非公平的,ReentrantLock 默認情況下也是非公平的,但可以通過帶布爾值的構造函數要求使用公平鎖。

3. 鎖綁定多個條件

  一個 ReentrantLock 對象可以同時綁定多個 Condition 對象。

ReentrantLock 有一個內部類 Sync,它繼承了 AbstractQueuedSynchronizer(下文簡稱“AQS”),抽象了鎖的獲取和釋放操作。Sync 有兩個實現類,分別是 FairSyncNonfairSync,分別公平鎖實現和非公平鎖實現。

一、基本使用

ReentrantLock 的使用十分簡單,如下所示。通過 lock() 方法加鎖,通過 unlock() 方法釋放鎖,爲了避免死鎖,釋放鎖應當放在 finally 塊中,確保鎖一定能夠釋放。

class X {
    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock(); 
	    try {
	        doSomething...
	      } finally {
	        lock.unlock()
	      }
	}
}

下面來簡單實驗下:

public class ReentrantLockDemo {
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(demo::func);
        executorService.execute(demo::func);
    }

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock();
        }
    }
}

// Output: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 

三、公平鎖與非公平鎖

ReentrantLock 的公平鎖和非公平鎖是通過構造方法實現的,默認無參情況下構造的是非公平鎖。

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

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

3.1 NonfairSync

3.1.1 Lock

首先我們來說下非公平鎖的獲取鎖操作。當調用 lock() 方法時,首先判斷 compareAndSetState(0, 1),該方法實際上做的事情就是對 state 變量做了一個 CAS 操作(利用反射實現),如果 state 值爲 0,就將其修改爲 1,且繼續執行 setExclusiveOwnerThread() 方法,那麼這個 state 是什麼呢?

// java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#compareAndSetState
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#stateOffset
private static final long stateOffset;
static {
    try {
        stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
    } catch (Exception ex) { throw new Error(ex); }
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#state
private volatile int state;

一開始在類圖中我們說過 Sync 繼承了 AQS, 在 AQS 類中,有一個 volatile 變量 state,它代表了ReentrantLock 的重入數。也就是說如果 ReentrantLock 沒有線程獨佔(state == 0),那就就將它獨佔(state = 1)。接下來看之後的 setExclusiveOwnerThread() 方法做了什麼。

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
 	protected AbstractOwnableSynchronizer() { }

    private transient Thread exclusiveOwnerThread;
 
    // java.util.concurrent.locks.AbstractOwnableSynchronizer#setExclusiveOwnerThread
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
 
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

如上所示,setExclusiveOwnerThread() 方法位於 AQS 的父類 AbstractOwnableSynchronizer(下文簡稱“AOS”)中,邏輯很簡單,就是記錄了獲取了獨佔鎖的線程(即當前線程)。


以上都是 CAS 成功的邏輯,如果 CAS 操作失敗,也就是說 ReentrantLock 已經被獨佔了,看看 acquire(1) 方法邏輯。

// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //中斷當前線程
        selfInterrupt();
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquire
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

可以看到 acquire() 方法是定義在 AQS 類中的,內部調用的 tryAcquire() 方法發現也是一個抽象方法,需要子類去具體實現,在非公平鎖中,tryAcquire() 方法的實現如下:

// java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
    // 獲取當前線程
    final Thread current = Thread.currentThread();
    // 獲取state(重入值)
    int c = getState();
    if (c == 0) { // state = 0,表示沒有線程獨佔鎖
        // 嘗試獨佔鎖
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) { // state != 0,表示已經有線程獨佔了鎖,判斷獨佔鎖的線程是否爲當前線程
        // 當前線程是獨佔鎖的線程,重入數+1
        int nextc = c + acquires;
        if (nextc < 0) // 超出最大重入數
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 當前線程不是獨佔鎖的線程
    return false;
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#state
private volatile int state;

protected final int getState() { return state; }

protected final void setState(int newState) { state = newState; }

nonfairTryAcquire() 方法首先根據 state 的值判斷 ReentrantLock 是否已經被獨佔了,如果沒有線程獨佔,將其獨佔。如果有線程獨佔了,如果當前線程就是 ReentrantLock 的獨佔者,那麼將重入的次數+1。

回到上面的 acquire() 方法,當 tryAcquire() 方法執行失敗,也就是獲取鎖失敗後,繼續執行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 來嘗試獲取鎖牽扯到 AQS 的同步隊列問題:

// java.util.concurrent.locks.AbstractQueuedSynchronizer#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;
            }
            /**
             * shouldParkAfterFailedAcquire:判斷線程可否安全掛起
             * parkAndCheckInterrupt:掛起線程並返回當時中斷標識Thread.interrupted()
             */
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

如果 acquire() 方法中的獲取鎖均失敗,執行 selfInterrupt() 方法,中斷當前線程:

// java.util.concurrent.locks.AbstractQueuedSynchronizer#selfInterrupt
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

3.1.2 UnLock

下面來看下重入鎖的釋放操作,底層調用 AQS 類的 release() 方法。

// java.util.concurrent.locks.ReentrantLock#unlock
public void unlock() {
    sync.release(1);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#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() 實現如下,代碼比較簡單,看註釋就應該能夠明白含義了:

// java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
protected final boolean tryRelease(int releases) {
    // 計算剩餘的 state 重入數
    int c = getState() - releases;
    // 當前線程不是鎖的所有者
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    
    // 是否釋放鎖(沒有線程獨佔)
    boolean free = false;
    if (c == 0) { // 沒有線程獨佔
        free = true;
        // 將鎖的獨佔線程清除
        setExclusiveOwnerThread(null);
    }
    
    // 更新 state
    setState(c);
    return free;
}

如果該方法返回 true,即代表鎖已經沒有線程獨佔了,下面的處理就是一些對 AQS 同步隊列的收尾工作,這裏暫且不做展開。

3.2 FairSync

說完了非公平鎖,下面來看看公平鎖的實現,公平鎖相較於非公平鎖主要的不同就是 lock() 方法的邏輯:

//java.util.concurrent.locks.ReentrantLock.FairSync#lock
final void lock() {
    acquire(1);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquire
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

以上的邏輯都是 AQS 類的邏輯,直接看 tryAcquire() 方法的公平鎖實現:

// java.util.concurrent.locks.ReentrantLock.FairSync#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;
}

該方法和非公平鎖的 nonfairTryAcquire() 比較,唯一不同的是判斷條件多了 hasQueuedPredecessors()方法,其定義如下:

// java.util.concurrent.locks.AbstractQueuedSynchronizer#hasQueuedPredecessors
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());
}

該方法是實現“公平”的具體邏輯。它對 AQS 同步隊列中當前節點是否有前驅節點進行判斷,如果該方法返回 true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之後才能繼續獲取鎖,以此來實現公平鎖。

3.3 測試

下面來分別測試下公平鎖和非公平鎖。

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private static CountDownLatch latch;

    public static void main(String[] args) {
        Lock fairLock = new MyReentrantLock(true);
        Lock unFairLock = new MyReentrantLock(false);

        testLock(fairLock);
    }

    private static void testLock(Lock lock) {
        latch = new CountDownLatch(1);
        for (int i = 0; i < 5; i++) {
            Thread thread = new Worker(lock, latch);
            thread.setName("Thread-" + i);
            thread.start();
        }
        latch.countDown();
    }
}

class Worker extends Thread {
    private Lock lock;
    private CountDownLatch latch;

    public Worker(Lock lock, CountDownLatch latch) {
        this.lock = lock;
        this.latch = latch;
    }

    @Override
    public void run() {
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 2; i++) {
            lock.lock();
            try {
                System.out.println("Lock by [" + getName() + "], Waiting by " + ((MyReentrantLock) lock).getQueuedThreads());
            } finally {
                lock.unlock();
            }
        }
    }

    @Override
    public String toString() {
        return getName();
    }
}

class MyReentrantLock extends ReentrantLock {
    MyReentrantLock(boolean fair) {
        super(fair);
    }

    @Override
    public Collection<Thread> getQueuedThreads() {
        List<Thread> arrayList = new ArrayList<>(super.getQueuedThreads());
        Collections.reverse(arrayList);
        return arrayList;
    }
}

當使用公平鎖運行時,輸出大致如下:

Lock by [Thread-3], Waiting by [Thread-4]
Lock by [Thread-4], Waiting by [Thread-0, Thread-1, Thread-2, Thread-3]
Lock by [Thread-0], Waiting by [Thread-1, Thread-2, Thread-3, Thread-4]
Lock by [Thread-1], Waiting by [Thread-2, Thread-3, Thread-4, Thread-0]
Lock by [Thread-2], Waiting by [Thread-3, Thread-4, Thread-0, Thread-1]
Lock by [Thread-3], Waiting by [Thread-4, Thread-0, Thread-1, Thread-2]
Lock by [Thread-4], Waiting by [Thread-0, Thread-1, Thread-2]
Lock by [Thread-0], Waiting by [Thread-1, Thread-2]
Lock by [Thread-1], Waiting by [Thread-2]
Lock by [Thread-2], Waiting by []

當使用非公平鎖運行時,輸出大致如下:

Lock by [Thread-3], Waiting by [Thread-4]
Lock by [Thread-3], Waiting by [Thread-4, Thread-1, Thread-0, Thread-2]
Lock by [Thread-4], Waiting by [Thread-1, Thread-0, Thread-2]
Lock by [Thread-4], Waiting by [Thread-1, Thread-0, Thread-2]
Lock by [Thread-1], Waiting by [Thread-0, Thread-2]
Lock by [Thread-1], Waiting by [Thread-0, Thread-2]
Lock by [Thread-0], Waiting by [Thread-2]
Lock by [Thread-0], Waiting by [Thread-2]
Lock by [Thread-2], Waiting by []
Lock by [Thread-2], Waiting by []

從上述結果可以看到,公平鎖每次都是隊列中的第一個節點獲取到鎖,而非公平鎖出現了一個線程連續獲取鎖的情況。

爲什麼會出現連續獲取鎖的情況呢?因爲在 nonfairTryAcquire(int) 方法中,每當一個線程請求鎖時,只要獲取了同步狀態就成功獲取了鎖。在此前提下,剛剛釋放鎖的線程再次獲取到同步狀態的機率很大,而其他線程只能在同步隊列中等待。

3.4 總結

事實上,公平鎖往往沒有非公平鎖的效率高,但是,並不是任何場景都是以 TPS 作爲唯一指標,公平鎖能夠減少“飢餓”發生的概率,等待越久的請求越能夠得到優先滿足。

非公平鎖有可能使線程飢餓,那爲什麼還要將它設置爲默認模式呢?我們再次觀察上面的運行結果,如果把每次不同線程獲取到鎖定義爲1次切換,公平鎖在測試中進行了10次切換,而非公平鎖只有5次切換,這說明非公平鎖的開銷更小。

四、lockInterruptibly & tryLock

4.1 lockInterruptibly

一開始就說過 ReentrantLock 支持等待可中斷。在使用 synchronized 時,阻塞在鎖上的線程除非獲得鎖否則將一直等待下去,也就是說這種無限等待獲取鎖的行爲無法被中斷。而 ReentrantLock 給我們提供了一個可以響應中斷的獲取鎖的方法 lockInterruptibly()

// java.util.concurrent.locks.ReentrantLock#lockInterruptibly
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireInterruptibly
public final void acquireInterruptibly(int arg)
    throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        // 沒有得到獨佔鎖後
        doAcquireInterruptibly(arg);
}

// java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireInterruptibly
private void doAcquireInterruptibly(int arg) throws InterruptedException {
    //將該結點尾插到 AQS 同步隊列
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            //獲取前置節點
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                // 這裏沒有中斷標識,lock和lockInterruptibly區別就是對中斷的處理方式 
                return;
            }
            
            //不斷自旋直至將前驅結點狀態設置爲SIGNAL,然後阻塞當前線程
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                // 無中斷標識,直接拋異常
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在前面介紹 lock() 方法時,其中的 acquireQueued() 方法在加鎖失敗後會設置一箇中斷標識 interrupted,死循環休眠加鎖。而 doAcquireInterruptibly() 方法相較於 acquireQueued() 方法取消了中斷標識,直接返回來實現響應中斷。

4.2 tryLock

獲取鎖除了使用 lock()lockInterruptibly () 這類阻塞方法以外,ReentrantLock 還提供了非阻塞加鎖方法,也就是 tryLock()

  1. tryLock()

    立即返回,獲取成功返回 true,獲取失敗返回 false。

  2. tryLock(long timeout, TimeUnit unit)

    在給定時間內,獲取成功返回 true,獲取失敗返回 false。

五、對比 Synchronized

5.1 相同點

1. 都是獨佔鎖

ReentrantLock 和 synchronized 都是獨佔鎖,只允許線程互斥的訪問臨界區。但是實現上兩者不同,synchronized 加鎖解鎖的過程是隱式的,用戶不用手動操作,優點是操作簡單,但顯得不夠靈活,ReentrantLock 需要手動加鎖和解鎖。

2. 都是可重入

ReentrantLock 和 synchronized 都是可重入的。synchronized 因爲可重入因此可以放在被遞歸執行的方法上,且不用擔心線程最後能否正確釋放鎖。而 ReentrantLock 在重入時要卻確保重複獲取鎖的次數必須和重複釋放鎖的次數一樣,否則可能導致其他線程無法獲得該鎖。

5.2 不同點

1. 鎖的實現

synchronized 是 JVM 實現的,而 ReentrantLock 是 JDK 實現的。

2. 性能

新版本 Java 對 synchronized 進行了很多優化,例如自旋鎖等。目前來看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是選擇 ReentrantLock 的理由。synchronized 有更大的性能優化空間,應該優先考慮 synchronized。

3. 功能

ReentrantLock 多了一些高級功能。

4. 使用選擇

除非需要使用 ReentrantLock 的高級功能,否則優先使用 synchronized。這是因爲 synchronized 是 JVM 實現的一種鎖機制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。並且使用 synchronized 不用擔心沒有釋放鎖而導致死鎖問題,因爲 JVM 會確保鎖的釋放。

六、參考資料

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