Java-併發-鎖-ReentrantLock
Java-併發-鎖-ReentrantLock
摘要
ReentrantLock
是使用最廣的、最出名的AQS(AbstractQueuedSynchronizer)
系列的可重入鎖。本文會分析他的lock
, unlock
等重要方法,還涉及公平/非公平概念對比,及對比synchronized。
關於Condition
的分析,請參見本文姊妹篇:Java-併發-Condition
0x01 基本概念
ReentrantLock
是使用最廣的、最出名的AQS(AbstractQueuedSynchronizer)
系列的可重入鎖。它相對於LockSupport
來說屬於是高層API,被鼓勵在使用者開發中使用。
ReentrantLock基本特點如下:
- 等待可中斷
獲取鎖時可以指定一個超時時間,如果超過這個時間還沒有拿到鎖就放棄等待 - 公平性
公平鎖就是按執行緒申請鎖時候FIFO的方式獲取鎖;而非公平鎖沒有這個規則,所有執行緒共同競爭,沒有先來後到一說 - 繫結物件
一個synchronized
繫結一個Object用來wait
,notify
等操作;而ReentrantLock可以newCondition多次等到多個Condition例項,執行await
,signal
等方法。
0x02 實現原理
限於篇幅,這裡可以大概說下其原理。
2.1 AQS
AQS全稱AbstractQueuedSynchronizer
ReentrantLock
內部類NonfairSync
和FairSync
的父類Sync
的父類,其核心元件如下:
- state,int 型別,用來儲存許可數
- Node雙向連結串列,儲存等待鎖的執行緒
該Node
就是AQS的內部類,這裡可以簡單看看Node定義:
static final class Node {
// 表明等待的節點處於共享鎖模式,如Semaphore:addWaiter(Node.SHARED)
static final Node SHARED = new Node();
// 表明等待的節點處於排他鎖模式,如ReentranLock:addWaiter(Node.EXCLUSIVE)
static final Node EXCLUSIVE = null;
// 執行緒已撤銷狀態
static final int CANCELLED = 1;
// 後繼節點需要unpark
static final int SIGNAL = -1;
// 執行緒wait在condition上
static final int CONDITION = -2;
// 使用在共享模式頭Node有可能處於這種狀態, 表示鎖的下一次獲取可以無條件傳播
static final int PROPAGATE = -3;
// 這個waitStatus就是存放以上int狀態的變數,預設為0
// 用volatile修飾保證多執行緒時的可見性和順序性
volatile int waitStatus;
// 指向前一個Node的指標
volatile Node prev;
// 指向後一個Node的指標
volatile Node next;
// 指向等待的執行緒
volatile Thread thread;
// condition_queue中使用,指向下一個conditionNode的指標
Node nextWaiter;
// 判斷是否共享鎖模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回前驅結點,當前驅結點為null時丟擲NullPointerException
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 用來初始化wait佇列的構造方法;也被用來做共享鎖模式
Node() {
}
// 在addWaiter方法時,將指定Thread以指定模式放置
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// Condition使用的構造方法
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS的Node等待佇列雙向連結串列如下圖:
2.2 非公平鎖的實現
預設採用非公平的實現NonFairSync
。
2.2.1 lock()
lock()
方法流程如下圖:
可以看到,lock()
方法最核心的部分就是可重入獲取許可(state),以及拿不到許可時放入一個AQS實現的雙向連結串列中,呼叫LockSupport.park(this)
將自己阻塞。就算阻塞過程被中斷喚醒,還是需要去拿鎖,直到拿到為止,注意,此時在拿到鎖之後還會呼叫selfInterrupt()
方法對自己發起中斷請求。
2.2.2 unlock()
2.3 公平鎖的實現
他的實現和非公平鎖有少許區別:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 這裡不再有非公平鎖的
// if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());
// 也就是說,公平鎖中,必須按規矩辦事,不能搶佔
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 這裡多了一個 !hasQueuedPredecessors(),也就是不需要考慮wait連結串列
// 否則就老實按流程走acquireQueued方法
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() {
// 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;
// h != t 代表wait連結串列不為空狀態
// (s = h.next) == null代表wait連結串列已經初始化
// s.thread != Thread.currentThread()代表當前執行緒不是第一個在wait連結串列排隊的執行緒
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
0x03 ReentrantLock和synchronized對比
ReentrantLock和synchronized對比如下:
可重入 | 等待可中斷 | 公平性 | 繫結物件數 | 效能優化 | |
---|---|---|---|---|---|
synchronized | 支援 | 不支援 | 非公平 | 只能1個 | 較多 |
ReentrantLock | 支援 | 支援 | 非公平/公平 | 可以多個 | - |
0x04 公平鎖與非公平鎖
非公平鎖比起公平鎖來說,唯一區別就是非公平鎖可以快速用compareAndSetState(0, acquires)
進行搶佔,而公平鎖必須老老實實FIFO形式排隊;但unlock
喚醒的時候是沒有區別的。
0x05 總結
- state採用
volatile
,保證有序性和可見性 - 大量使用如
unsafe.compareAndSwapInt(this, stateOffset, expect, update);
此類的CAS操作,保證原子性,同時在競爭小的時候效率勝過synchronized
- 所謂的加鎖就是AQS.state++。且該鎖是可重入的,每次就state加1,unlock一次減一。兩個操作必須一一對應,否則其他等待鎖的執行緒永遠等待。
- 所謂的等待鎖阻塞,就是放在一個連結串列裡,然後用
LockSupport.park(this)
阻塞 - 就算用中斷喚醒已經等待鎖而阻塞的執行緒,依然必須直到獲取鎖才能執行。且在其後如果執行可中斷操作,會發生中斷!
關於Condition
的分析,請參見本文姊妹篇:Java-併發-Condition