一、簡介
ReentrantLock
是一個可重入且獨佔式的鎖,相較於傳統的 Synchronized
,它增加了輪詢、超時、中斷等高級功能。其類圖如下:
ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖,相比於 synchronized,它多了以下高級功能:
1. 等待可中斷
當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,改爲處理其他事情。
2. 可實現公平鎖
公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。
synchronized 中的鎖是非公平的,ReentrantLock 默認情況下也是非公平的,但可以通過帶布爾值的構造函數要求使用公平鎖。
3. 鎖綁定多個條件
一個 ReentrantLock 對象可以同時綁定多個 Condition 對象。
ReentrantLock 有一個內部類 Sync
,它繼承了 AbstractQueuedSynchronizer(下文簡稱“AQS”),抽象了鎖的獲取和釋放操作。Sync 有兩個實現類,分別是 FairSync
和 NonfairSync
,分別公平鎖實現和非公平鎖實現。
一、基本使用
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()
。
-
tryLock()
立即返回,獲取成功返回 true,獲取失敗返回 false。
-
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 會確保鎖的釋放。