簡介
StampedLock 是JDK1.8 開始提供的一種鎖, 是對之前介紹的讀寫鎖 ReentrantReadWriteLock 的功能增強。StampedLock 有三種模式:Writing(讀)、Reading(寫)、Optimistic Reading(樂觀度),StampedLock 的功能不是基於AQS來實現的,而是完全自己內部實現的功能,不支援重入。在加鎖的時候會返回一個戳,解鎖的時候需要傳入,匹配完成解鎖操作。
官方使用示例
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
// 寫鎖-獨佔資源
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
// 只讀的方法,比較樂觀,認為讀的過程中不會有寫,所以這裡是樂觀度
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) { // 檢查樂觀讀鎖後是否有其他寫鎖發生
// 獲取一個普通的讀鎖
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
// 釋放讀鎖
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
// 普通讀鎖轉換成寫鎖,返回0為轉換失敗
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}
}
官方demo中用到的 api 主要有獲取寫鎖(writeLock()
)、釋放寫鎖(unlockWrite(stamp)
)、獲取普通讀鎖(readLock()
)、釋放普通讀鎖(unlockRead(stamp)
)、獲取樂觀讀鎖(tryOptimisticRead()
)、檢測樂觀讀版本(validate(stamp)
)、普通讀鎖轉換成寫鎖(tryConvertToWriteLock(stamp)
)。下面分析原始碼的時候也主要根據這幾個方法來分析。
原始碼分析
主要內部類
-
等待節點:WNode
用於維護 CLH 佇列的節點,原始碼如下:static final class WNode { volatile WNode prev; // 前驅節點 volatile WNode next; // 後繼節點 volatile WNode cowait; // 連結的讀者列表 volatile Thread thread; // 執行緒 volatile int status; // 狀態 0, WAITING, or CANCELLED final int mode; // 兩種模式:RMODE or WMODE WNode(int m, WNode p) { mode = m; prev = p; } }
-
ReadWriteLockView:實現了ReadWriteLock介面,提供了讀寫鎖獲取介面
final class ReadWriteLockView implements ReadWriteLock { public Lock readLock() { return asReadLock(); } public Lock writeLock() { return asWriteLock(); } }
-
ReadLockView 和 WriteLockView:都實現了Lock介面,並實現了所有的方法
主要屬性
-
CLH 佇列
/** Head of CLH queue */ private transient volatile WNode whead; /** Tail (last) of CLH queue */ private transient volatile WNode wtail;
-
其他常量屬性
/** CPU的核心數量,用來控制自旋的次數 */ private static final int NCPU = Runtime.getRuntime().availableProcessors(); /** 獲取鎖入隊前最大重試次數:CPU核心數大於1:64,否則0 */ private static final int SPINS = (NCPU > 1) ? 1 << 6 : 0; /** 等待佇列的頭結點,獲取鎖最大重試次數:CPU核心數大於1:1024,否則0 */ private static final int HEAD_SPINS = (NCPU > 1) ? 1 << 10 : 0; /** 再次阻塞前最大重試次數:CPU核心數大於1:65536,否則0 */ private static final int MAX_HEAD_SPINS = (NCPU > 1) ? 1 << 16 : 0; /** The period for yielding when waiting for overflow spinlock */ private static final int OVERFLOW_YIELD_RATE = 7; // must be power 2 - 1 /** 用於讀取器計數的位數 */ private static final int LG_READERS = 7; // 用來計算state值的常量 private static final long RUNIT = 1L; // 讀單位 // 寫鎖的標識位 十進位制:128 二進位制位標示:1000 0000 private static final long WBIT = 1L << LG_READERS; // 讀狀態標識 admol 十進位制:127 二進位制: 0111 1111 private static final long RBITS = WBIT - 1L; // 讀鎖的最大標識 十進位制:126 二進位制 :0111 1110 private static final long RFULL = RBITS - 1L; // 用來讀取讀寫狀態 十進位制:255 二進位制:1111 1111 private static final long ABITS = RBITS | WBIT; // ~255 == 11111111111111111111111111111111111111111111111111111111 1000 0000 // -128 private static final long SBITS = ~RBITS; // 同步狀態state的初始值 256 二進位制:0001 0000 0000 private static final long ORIGIN = WBIT << 1; // 中斷 private static final long INTERRUPTED = 1L; // 節點的狀態 private static final int WAITING = -1; private static final int CANCELLED = 1; // 節點的模式 private static final int RMODE = 0; private static final int WMODE = 1; /** 同步狀態 初始值 256 0001 0000 0000*/ private transient volatile long state; /** 讀計數飽和時的額外讀取器計數 */ private transient int readerOverflow;
StampedLock 雖然沒有繼承AQS,但是屬性上很相似,都有一個CLH佇列,和一個同步狀態值state, StampedLock用8位來表示讀寫鎖狀態,前7位是用來標識讀鎖狀態的,第8位標識寫鎖佔用,如果讀鎖數量超過了126(0111 1110 ),超出的用readerOverflow來計數。
構造方法
public StampedLock() {
// 初始值 256, 二進位制:0001 0000 0000
state = ORIGIN;
}
獲取寫鎖:writeLock()
原始碼展示:
public long writeLock() {
long s, next; // bypass acquireWrite in fully unlocked case only
return ((((s = state) & ABITS) == 0L && U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? next : acquireWrite(false, 0L));
}
程式碼分析:
- 首先執行的是
((s = state) & ABITS)== 0L
,用來表示讀鎖和寫鎖是否可以被獲取
解析:第一次時,state 初始值是256,ABITS是255,計算過程:0001 0000 0000 && 0000 1111 1111 ,結算結果為0。 - 執行CAS操作:
U.compareAndSwapLong(this, STATE, s, next = s + WBIT))
解析:STATE
是state欄位的記憶體地址相對於此物件的記憶體地址的偏移量,s
是期望值,next = s + WBIT
是更新值;s+WBIT 也就是 256 + 128,next計算結果為384,用二進位制位表示就是0001 1000 0000
,也就是將第8位設定為1,就是獲得寫鎖。如果CAS 更新成功,返回next值,成功獲得寫鎖。 - 如果CAS執行失敗,則執行
acquireWrite(false, 0L)
,進入等待佇列獲取鎖
acquireWrite
程式碼分析:
// interruptible 是否要檢查中斷
// deadline:0 一直等待獲取鎖
private long acquireWrite(boolean interruptible, long deadline) {
// node:即將入隊排隊的節點
// p:當前排隊節點入隊之前的尾節點
WNode node = null, p;
// 第一次自旋:排隊節點入佇列自旋
for (int spins = -1;;) { // spin while enqueuing
long m, s, ns;
// 這個if 和外面一樣的,從程式碼執行到這期間所有沒有被釋放
if ((m = (s = state) & ABITS) == 0L) {
// CAS 再次嘗試獲取下寫鎖
if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT))
// 成功獲取寫鎖
return ns;
} else if (spins < 0) // 走到這,說明上面還是沒獲取到寫鎖,寫鎖被佔用了,m的值為128
// 1. 確定自旋的次數 spins: 64 or 0
spins = (m == WBIT && wtail == whead) ? SPINS : 0;
else if (spins > 0) {
// 2.自旋的次數大於0,隨機減一次自旋次數,直到減到spins為0(by.精靈王 這裡實際是空轉,沒什麼特點的邏輯處理)
if (LockSupport.nextSecondarySeed() >= 0)
--spins;
} else if ((p = wtail) == null) { // initialize queue
// 3.自旋spins減到0後會立馬執行到這裡
// p被賦值為尾節點
// 初始化佇列, WMODE:寫,null:前驅節點
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd))
// 初始化佇列時,尾節點等於頭節點
wtail = hd;
} else if (node == null)
// 4.初始化佇列後,下一次自旋,構建當前排隊節點,並指定了其尾節點
node = new WNode(WMODE, p);
else if (node.prev != p) // 如果當前節點的前驅不是尾節點
// 5.前驅節點設定為之前佇列的尾節點
node.prev = p;
else if (U.compareAndSwapObject(this, WTAIL, p, node)) {
// 6. CAS 更新尾節點為當前排隊的節點
p.next = node;
// 退出自旋
break;
}
}
// 第二次自旋
for (int spins = -1;;) {
WNode h, np, pp; int ps;
if ((h = whead) == p) { // 如果頭節點和之前的尾節點p是同一個, 說明馬上應該輪到node節點獲得鎖
if (spins < 0)
// ① 設定自旋次數 1024 or 0
spins = HEAD_SPINS;
else if (spins < MAX_HEAD_SPINS)
// 第一次自旋1024次沒有獲取到鎖,這次自旋翻倍
// 自旋次數*2 2048 繼續進入到下面的自旋
spins <<= 1;
for (int k = spins;;) { // spin at head
// ② 第三次自旋,不斷嘗試獲得鎖(自旋1024或者2048次),直到成功獲得鎖 或者 break
long s, ns;
// ((s = state) & ABITS) == 0L 表示鎖沒有被佔用
if (((s = state) & ABITS) == 0L) {
// CAS 修改state值
if (U.compareAndSwapLong(this, STATE, s, ns = s + WBIT)) {
// CAS 修改成功獲得鎖,設定新的頭結點
whead = node;
node.prev = null;
return ns;
}
} else if (LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
// 隨機立減自旋次數 自旋次數為0時跳出自旋迴圈
break;
}
} else if (h != null) { // help release stale waiters
// 頭節點不為空
// 進入情景:寫鎖被獲取,佇列中很多等待獲取讀鎖的執行緒,寫鎖釋放,讀鎖被喚醒後可能進入到這裡
WNode c; Thread w;
while ((c = h.cowait) != null) { // 自旋
// 頭節點的 cowait不為空
// h.cowait 修改成節點的下一個cowait
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) && (w = c.thread) != null)
// 喚醒 cowait 裡面的執行緒
U.unpark(w);
}
}
if (whead == h) { // 如果頭結點沒有變化
// P 是之前的尾節點
if ((np = node.prev) != p) {
// != 之前的尾節點,也就是說當前節點的前驅節點不是尾節點時
if (np != null)
// 儲存尾節點和當前節點的連線關係
(p = np).next = node; // stale
}
else if ((ps = p.status) == 0)
// ③ 上面第三次自旋break後會進入到這裡,修改尾節點狀態
// 更新尾節點的狀態為WAITING:-1, 然後繼續回到第二次自旋的地方,重新開始自旋
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
else if (ps == CANCELLED) {
// p節點狀態是取消,則刪除p節點
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
} else {
// 超時時間
long time; // 0 argument to park means no timeout
if (deadline == 0L)
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L)
// 設定了超時時間 已經超時,取消當前node節點
return cancelWaiter(node, node, false);
Thread wt = Thread.currentThread();
// 為給定地址設定值,忽略修飾限定符的訪問限制,與此類似操作還有: putInt,putDouble,putLong,putChar等
U.putObject(wt, PARKBLOCKER, this);
// node節點指向當前執行緒
node.thread = wt;
// p.status < 0 的只有-1 也就是WAITING
if (p.status < 0 && (p != h || (state & ABITS) != 0L) && whead == h && node.prev == p)
// 阻塞當前執行緒
U.park(false, time); // emulate LockSupport.park
// 執行緒被喚醒後,清除節點的執行緒
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted()) // 要檢查中斷 && 執行緒有被中斷
// 取消當前node節點
return cancelWaiter(node, node, true);
}
}
}
}
獲取寫鎖過程總結:
- 首先檢查鎖有沒有被佔用
- 沒有被佔用,嘗試 CAS 修改state值,CAS 修改成功則獲得鎖,返回新的 state 值。
- CAS 修改失敗的話進入下面自旋的邏輯
- 第一層自旋(節點入隊):
- 先檢查下鎖有沒有被佔用(
(m = (s = state) & ABITS) == 0L
), CAS 嘗試一下獲取鎖,獲取失敗再繼續自旋 - 入隊之前會自旋64次(CPU核心數大於1),期間不做任何處理
- 初始化排隊佇列的隊頭隊尾節點,當前節點加入到隊尾,CAS 更新尾節點,更新成功則退出第一次自旋
- 先檢查下鎖有沒有被佔用(
- 開始第二層自旋(嘗試獲取鎖,阻塞執行緒),第二層自旋和第三層自旋巢狀執行的:
- 如果頭節點和之前的尾節點p還是是同一個(沒有其他獲取鎖的節點排隊已經入隊), 說明馬上應該輪到node節點獲得鎖(排隊的只有node節點)。
- 初始化第三層自旋次數(第一次1024,第二次2048),開啟第三層自旋
- 位運算檢查鎖是否有被釋放(
(s = state) & ABITS) == 0L
),CAS 修改 state 值,修改成功,退出,返回新的state值
- 位運算檢查鎖是否有被釋放(
- 這裡其實就是自旋和park執行緒之間效能的一個權衡,馬上就要獲得鎖了,是自旋還是阻塞執行緒繼續等,這裡選擇了先自旋1024次,如果沒有獲得鎖,繼續自旋2048次,如果還是沒獲得鎖,則退出第三層自旋,回到第二層自旋,準備阻塞當前執行緒。
- 初始化第三層自旋次數(第一次1024,第二次2048),開啟第三層自旋
- 如果排隊的頭結點不為空,檢查頭結點的cowait 連結串列,如果不為空,自旋 CAS 修改頭節點的cowait, 嘗試喚醒整個鏈的節點執行緒
- 第三層自旋完成後還是沒有獲取到鎖,阻塞當前執行緒,等待被喚醒,被喚醒後繼續第二層自旋獲取鎖,重複這個過程,直到獲取鎖成功推出。
- 如果頭節點和之前的尾節點p還是是同一個(沒有其他獲取鎖的節點排隊已經入隊), 說明馬上應該輪到node節點獲得鎖(排隊的只有node節點)。
釋放寫鎖:unlockWrite(stamp)
public void unlockWrite(long stamp) {
WNode h;
// state != stamp 檢查解鎖與加鎖的版本是否匹配
// (stamp & WBIT) == 0L 為true的話說明鎖沒有被佔用
if (state != stamp || (stamp & WBIT) == 0L)
// 丟擲異常
throw new IllegalMonitorStateException();
// 釋放寫鎖,會增加state的版本
// stamp += WBIT 等於二進位制(第一次加寫鎖和解鎖) 0001 1000 0000 + 0000 1000 0000 == 0010 0000 0000
// 解鎖會把stamp 的二進位制第8位設定為0
// 相當於重新賦值state值
state = (stamp += WBIT) == 0L ? ORIGIN : stamp;
if ((h = whead) != null && h.status != 0) // 頭結點不為 && 狀態不為初始狀態0(一般是WAITING -1),說明佇列中有排隊獲取鎖的執行緒
// 喚醒頭節點的後繼節點
release(h);
}
private void release(WNode h) {
if (h != null) {
// q節點:頭節點的有效後繼節點
// w: 需要喚醒的執行緒
WNode q; Thread w;
// 將頭節點的狀態設定成0
U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
if ((q = h.next) == null || q.status == CANCELLED) { // 如果頭節點的後繼為空 或者 是取消狀態
// 就從排隊的隊尾找一個有效的節點
for (WNode t = wtail; t != null && t != h; t = t.prev)
if (t.status <= 0)
q = t;
}
// 找到了有效的節點,喚醒其執行緒
if (q != null && (w = q.thread) != null)
U.unpark(w);
}
}
釋放寫鎖過程總結:
- 檢查鎖印章戳是否匹配,鎖是否有被佔用,檢查不通過丟擲異常
- 通過位運算
stamp += WBIT
計算新的state值,state 二進位制位的第8位會被設定成0就是寫鎖解鎖 - 檢測佇列中是否有排隊獲取鎖的執行緒
- 喚醒下一個等待獲取鎖的執行緒(
unpark(thread)
)
- 喚醒下一個等待獲取鎖的執行緒(
獲取普通讀鎖:readLock()
public long readLock() {
long s = state, next; // bypass acquireRead on common uncontended case
// 在沒有執行緒獲得鎖的情況下,s的初始值是256
// whead == wtail 為true:表示佇列為空
// (s & ABITS) < RFULL: 已獲取讀鎖的數小於最大值126
return ((whead == wtail && (s & ABITS) < RFULL && U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
? next : acquireRead(false, 0L));
}
程式碼解析:
- 佇列為空 && 已獲取讀鎖次數小於126 && CAS 修改 state 值
- 條件完全成立,成功獲得鎖,返回新的state值
- 沒有成功,進入到
acquireRead(false, 0L)
方法排隊獲取鎖
acquireRead
原始碼展示(程式碼超100行,需耐心觀看):
private long acquireRead(boolean interruptible, long deadline) {
// p節點為尾節點 node為入隊節點
WNode node = null, p;
// 第一層大迴圈 第一次自旋,是不是和獲取寫鎖的很像?
for (int spins = -1;;) {
WNode h;
if ((h = whead) == (p = wtail)) { // 首尾節點相等,說明佇列為空,有執行緒在排隊不會進入if
// 前面沒獲取到鎖,佇列又為空,是不是應該馬上就是當前執行緒獲取鎖了?
// 第二次自旋 自旋64次 目的是為了看馬上能不能獲取鎖(排隊佇列為空,沒執行緒排隊時,會在這裡自旋獲取鎖)
for (long m, s, ns;;) {
// 這裡是個三目運算,程式碼太長,拆開來看
// (m = (s = state) & ABITS) < RFULL;和進入readLock()方法時的條件一樣,判斷讀鎖的數是否達到最大值,只有寫鎖被獲取,這裡就是false
// 我們假設前面寫鎖被獲取了,現在獲取讀鎖,m 就是128,大於RFULL 126
// 沒有達到最大值, CAS 修改狀態值 U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT)
// 超過最大值了,記得前面那個readerOverflow屬性不?在tryIncReaderOverflow這累加
if ((m = (s = state) & ABITS) < RFULL ? U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
return ns;
else if (m >= WBIT) {// if條件成立,說明說明被佔用
//
if (spins > 0) {
if (LockSupport.nextSecondarySeed() >= 0)
--spins; // 隨機減自旋次數
} else {
if (spins == 0) { // 自旋次數減到0了,還沒獲取到讀鎖
WNode nh = whead, np = wtail;
if ((nh == h && np == p) || (h = nh) != (p = np))
break; // 退出自旋
}
spins = SPINS; // 初始自旋次數 64次
}
}
}
// 上面到這裡都是在處理佇列為空,馬上要獲取到鎖的情況
}
if (p == null) { // 尾節點為空,初始化排隊佇列,有執行緒在排隊時不會進入到這裡
WNode hd = new WNode(WMODE, null);
if (U.compareAndSwapObject(this, WHEAD, null, hd)) // CAS 設定頭節點
// 執行到這裡後,會回到第一次的自旋,再次進入到第二次自旋,這次 spins 為0,只會自旋一次
wtail = hd;
} else if (node == null)
// 初始化當前入隊排隊節點,有執行緒在排隊時,直接進入到這裡,然後繼續第一次自旋
node = new WNode(RMODE, p);
else if (h == p || p.mode != RMODE) { // 佇列為空 或者 尾節點不是讀模式
// 排隊節點入隊
if (node.prev != p)
node.prev = p; // 設定排隊節點的前驅節點
// 繼續第一次自旋
else if (U.compareAndSwapObject(this, WTAIL, p, node)) { // CAS 修改尾節點
p.next = node; // 老的尾節點的後繼節點為當前節點
// 進入到這裡會退出第一層自旋, 直接進入到下面第二層的大的自旋
break;
}
} else if (!U.compareAndSwapObject(p, WCOWAIT, node.cowait = p.cowait, node))
// 上面那個if分支進不去,只有條件:佇列不為空 and 尾節點是讀模式 為真
// 進入到了這裡,說明CAS失敗
// 這裡的CAS 就是把當前節點加入到尾節點的cowait棧裡面
// 從這裡可以看出加入的順序是個棧結構,先把舊的尾節點的cowait賦值給node節點的cowait,然後再把node節點賦值給尾節點
node.cowait = null;
else {
// 進入到這,說明上面的if分支都沒有進去,尾節點不為空,當前節點不為空,佇列不為空,尾節點是讀模式,上面CAS修改成功
// 總結一下進入到這裡的條件就是,有個執行緒獲得了寫鎖還沒釋放,佇列中有讀執行緒在排隊
// 第三次自旋,有執行緒在排隊獲取鎖時,會進入到這裡自旋
for (;;) {
WNode pp, c; Thread w;
if ((h = whead) != null && (c = h.cowait) != null &&
U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null) // help release
// 頭節點不為空,且其cowait節點不為空,喚醒整個cowait棧的執行緒
U.unpark(w);
if (h == (pp = p.prev) || h == p || pp == null) {
// 頭節點等於尾節點的前驅節點 或者頭節點等於尾節點 或者 尾節點的前驅節點為空
// 說明還是馬上輪到自己獲得鎖
long m, s, ns;
do {
// 判斷是否可以使用CAS獲取讀鎖
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L))
return ns;
} while (m < WBIT); // m < WBIT時表示寫鎖沒有被佔用,一直嘗試獲取鎖
}
if (whead == h && p.prev == pp) { // 對頭沒有發生變化 ,隊尾也沒發生變化
long time;
if (pp == null || h == p || p.status > 0) { // 隊尾的前驅節點為空或者 頭節點等於尾節點 或者 老的尾節點被取消(>0的狀態只有1,取消)
node = null; // 拋棄當前節點,退出當前迴圈,回到第一層的自旋,重新構建節點
break;
}
if (deadline == 0L)
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L) // 超時
return cancelWaiter(node, p, false); // 取消節點
Thread wt = Thread.currentThread();
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
if ((h != pp || (state & ABITS) == WBIT) &&
whead == h && p.prev == pp)
U.park(false, time); // 阻塞當前執行緒
// 執行緒被喚醒,開始繼續自旋獲取鎖
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted())
return cancelWaiter(node, p, true);
}
}
}
}
// 第二層大的迴圈
for (int spins = -1;;) {
WNode h, np, pp; int ps;
if ((h = whead) == p) { // 如果佇列為空,說明馬上輪到當前執行緒獲得鎖了
// 這個大的if 裡面做的就是獲取鎖
if (spins < 0)
// 初始化本次自旋獲取鎖的次數:1024次
spins = HEAD_SPINS;
else if (spins < MAX_HEAD_SPINS)
// 上面1024次自旋沒有獲取到鎖,就自旋翻倍:2048次,繼續下面的自旋
spins <<= 1;
// 開始自旋獲取鎖
for (int k = spins;;) { // spin at head
long m, s, ns;
// 這個if條件 檢查了是否可以獲取鎖,如果可以就CAS獲取鎖
if ((m = (s = state) & ABITS) < RFULL ?
U.compareAndSwapLong(this, STATE, s, ns = s + RUNIT) :
(m < WBIT && (ns = tryIncReaderOverflow(s)) != 0L)) {
// 進入到這個if裡面,說明就獲得了鎖
WNode c; Thread w;
// 這裡的node節點是還沒有繫結thread的
whead = node;
node.prev = null;
// 要喚醒當前node節點中的所有cowait節點執行緒
// 當前節點是在上面的第一層大的自旋入隊的,其他獲取讀鎖的節點都是掛在這個節點的cowait下的
while ((c = node.cowait) != null) {
if (U.compareAndSwapObject(node, WCOWAIT,
c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w); // 喚醒執行緒
}
return ns;
} else if (m >= WBIT && LockSupport.nextSecondarySeed() >= 0 && --k <= 0)
// 上面沒有獲取到鎖,自旋減次數,直到為0,退出自旋
break;
}
} else if (h != null) { // 佇列不為空,頭節點不為空
WNode c; Thread w;
while ((c = h.cowait) != null) {
if (U.compareAndSwapObject(h, WCOWAIT, c, c.cowait) &&
(w = c.thread) != null)
U.unpark(w);
}
}
// 執行到這,說明還沒獲取到鎖
if (whead == h) { // 頭節點沒變過
if ((np = node.prev) != p) { // 檢查節點的連結關係
if (np != null)
(p = np).next = node; // stale
}
else if ((ps = p.status) == 0)
// 檢查尾節點的狀態,為0則更新成-1,回到第二層大的迴圈開始處
U.compareAndSwapInt(p, WSTATUS, 0, WAITING);
else if (ps == CANCELLED) { // p節點被取消,刪除這個節點
if ((pp = p.prev) != null) {
node.prev = pp;
pp.next = node;
}
} else { // 上面2048次自旋後還是沒獲取到鎖,進入到最終阻塞執行緒的環節
long time;
if (deadline == 0L)
time = 0L;
else if ((time = deadline - System.nanoTime()) <= 0L)
return cancelWaiter(node, node, false); // 超時了就取消當前節點
Thread wt = Thread.currentThread(); // 當前執行緒
U.putObject(wt, PARKBLOCKER, this);
node.thread = wt;
if (p.status < 0 && (p != h || (state & ABITS) == WBIT) && whead == h && node.prev == p)
U.park(false, time); // 阻塞執行緒
// 執行緒被喚醒了,繼續執行 第二層大的自旋獲取鎖
node.thread = null;
U.putObject(wt, PARKBLOCKER, null);
if (interruptible && Thread.interrupted()) // 被喚醒了,發現要求中斷執行緒 並且執行緒被中斷了
return cancelWaiter(node, node, true); // 取消當前節點
}
}
}
}
獲取普通讀鎖總結:
- 位運算檢查寫鎖是否被佔用,讀鎖是否超限制
- 滿足條件,直接CAS 修改state值,並返回新的state值
- 開始第一層大的自旋,第一層大的自旋里麵包含了兩種情況不同的自旋:
- 第一種自旋情況:排隊的佇列為空,沒有其他執行緒在排隊等待鎖時
- 這種情況說明,鎖雖然已經被佔用,但是馬上就應該是我得到鎖了,所以我先在這兒自旋(64次)等等你釋放鎖,免得阻塞我自己,之後喚醒還需要成本
- 自旋沒有獲取到鎖,會退出第一層大的自旋,進入到第二層大的自旋
- 第二種自旋情況:寫鎖被佔用,排隊的佇列不為空,隊尾是讀模式時
- 這種情況,會不斷嘗試獲取鎖,阻塞執行緒,等待被喚醒,一直在這個自旋里面,直到獲得鎖,或者超時中斷被取消,不會進入到第二層大的自旋
- 第一種自旋情況:排隊的佇列為空,沒有其他執行緒在排隊等待鎖時
- 第二層大的自旋
- 這一層的自旋是對第一層裡面第一種自旋情況(馬上輪到我獲得鎖,但是前面持有鎖的執行緒就是不釋放)的補充,因為沒有執行緒在排隊,只要前面的執行緒釋放了鎖,馬上就可以獲得鎖了,所以這一層還是在自旋獲得鎖,只不過自旋次數有增加
- 首先會嘗試自旋1024次獲得鎖,如果前面還沒釋放鎖,再自旋2048次
- 如果2048次之後還是沒有等到前面的鎖釋放,就阻塞當前執行緒,等待被喚醒,直到獲得鎖,或者超時中斷被取消
- 這一層的自旋是對第一層裡面第一種自旋情況(馬上輪到我獲得鎖,但是前面持有鎖的執行緒就是不釋放)的補充,因為沒有執行緒在排隊,只要前面的執行緒釋放了鎖,馬上就可以獲得鎖了,所以這一層還是在自旋獲得鎖,只不過自旋次數有增加
cowait棧分析
下面程式碼是main執行緒先獲取寫鎖不釋放,之後T0,T1,T2,T3執行緒先後去獲取讀鎖,最後斷點觀察整個排隊佇列的情況
StampedLock sl = new StampedLock();
long stamp = sl.writeLock();
// 先讓T0執行緒去排隊到尾節點
TimeUnit.SECONDS.sleep(1);
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
long stamp = sl.readLock();
System.out.println(stamp + " x");
}
},"T0").start();
// 之後T1執行緒來獲取讀
TimeUnit.SECONDS.sleep(3);
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
long stamp = sl.readLock();
System.out.println(stamp + " x");
}
},"T1").start();
TimeUnit.SECONDS.sleep(3);
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
long stamp = sl.readLock();
System.out.println(stamp + " x");
}
},"T2").start();
TimeUnit.SECONDS.sleep(3); // 在這裡先斷點,進入到這裡後,再到原始碼位置去斷點,就可以看到如下圖的情況了
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
long stamp = sl.readLock();
System.out.println(stamp + " x");
}
},"T3").start();
執行程式碼後,斷點截圖:
他們的關係可以用如下表示,橫向是連結串列,縱向是cowait棧。
釋放讀鎖:unlockWrite(stamp)
public void unlockRead(long stamp) {
long s, m; WNode h;
for (;;) { // 自旋
if (((s = state) & SBITS) != (stamp & SBITS) || (stamp & ABITS) == 0L || (m = s & ABITS) == 0L || m == WBIT)
// 檢查版本
throw new IllegalMonitorStateException();
if (m < RFULL) { // 鎖標識小於讀鎖的最大標識
if (U.compareAndSwapLong(this, STATE, s, s - RUNIT)) { // CAS 更新state值
if (m == RUNIT && (h = whead) != null && h.status != 0) // 頭結點不為空
release(h); // 喚醒下一個節點
break;
}
} else if (tryDecReaderOverflow(s) != 0L)
// 讀鎖個數飽和溢位,嘗試減少readerOverflow
break;
}
}
private void release(WNode h) {
if (h != null) {
WNode q; Thread w;
U.compareAndSwapInt(h, WSTATUS, WAITING, 0);
if ((q = h.next) == null || q.status == CANCELLED) {
for (WNode t = wtail; t != null && t != h; t = t.prev)
if (t.status <= 0)
q = t;
}
if (q != null && (w = q.thread) != null)
U.unpark(w);
}
}
釋放讀鎖的邏輯也比較簡單,和釋放寫鎖的邏輯很相識,喚醒下一個節點的release方法也完全一致
獲取樂觀讀鎖:tryOptimisticRead()
public long tryOptimisticRead() {
long s;
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
樂觀讀鎖的邏輯也比較簡單,就一個三目運算,((s = state) & WBIT) == 0L
就是看寫鎖是否有被佔用,寫鎖被佔用返回0,否則返回寫鎖沒被佔用的包含高位版本有效戳(也就是寫鎖的版本)。
檢測樂觀讀版本:validate(stamp)
public boolean validate(long stamp) {
// 插入記憶體屏障,禁止load操作重排序。
// 由於StampedLock提供的樂觀讀鎖不阻塞寫執行緒獲取讀鎖,當執行緒共享變數從主記憶體load到執行緒工作記憶體時,會存在資料不一致問題
// 解決鎖狀態校驗運算髮生重排序導致鎖狀態校驗不準確的問題
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
返回true:表示期間沒有寫鎖發生,讀鎖為所謂
返回false:表示期間有寫鎖發生
那這裡是怎麼計算的呢?
SBITS為-128,用二進位制表示是:1111 1111 1111 1000 0000
兩種情況:
- 假如先獲取樂觀鎖,再獲取讀鎖;
樂觀鎖返回的stamp為256,二進位制位是 0001 0000 0000;
獲取讀鎖之後state值是257,二進位制位是 0001 0000 0001;
它們分別於與-128 進行與運算後都是0001 0000 0000,也就是十進位制256,返回true; - 假如先獲取樂觀鎖,再獲取寫鎖;
樂觀鎖返回的stamp為256,二進位制位是 0001 0000 0000;
獲取寫鎖之後state值是384,二進位制位是 0001 1000 0000;
它們分別與-128 進行與運算後,相當與 256 == 384,結果肯定返回false;
普通讀鎖轉換成寫鎖:tryConvertToWriteLock(stamp)
public long tryConvertToWriteLock(long stamp) {
// m標識最新的鎖標識
// a標識被轉換的鎖的鎖標識
long a = stamp & ABITS, m, s, next;
while (((s = state) & SBITS) == (stamp & SBITS)) { // 檢查鎖持有狀態
if ((m = s & ABITS) == 0L) {
if (a != 0L)
break;
if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT))
return next;
} else if (m == WBIT) { // 寫鎖已經被佔用
if (a != m)
break;
return stamp; // 說明被轉換前就是寫鎖
} else if (m == RUNIT && a != 0L) { // 被轉換前的是普通讀鎖,寫鎖沒被佔用
if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT + WBIT))
// s:是之前的鎖狀態
// s - RUNIT:就是釋放讀鎖
// + WBIT :就是加寫鎖(進入之前寫鎖沒被佔用)
return next; // 返回最新的鎖狀態
} else
break; // 其他情況,全部返回0,轉換失敗
}
return 0L; // 返回0,標識轉換寫鎖失敗
}
普通讀鎖轉換成寫鎖過程總結:
- 如果轉換前是寫鎖,直接返回寫鎖
- 如果轉換前是讀鎖,轉換期間,寫鎖被佔用,返回0,轉換失敗
- 如果轉換前是讀鎖,寫鎖沒有被佔用,釋放讀鎖,加寫鎖,返回寫鎖,轉換成功
- 其他情況,全部返回0,轉換失敗
StampedLock 總結
- StampedLock 是一種支援樂觀讀鎖的高階版讀寫鎖
- StampedLock 沒有使用AQS 同步框架,而是完全自己實現的同步狀態state 和 CLH佇列維護演算法
- 同步狀態
state
的低7位標識讀鎖的數量,第8位標識寫鎖是否被佔用,高24位記錄寫鎖的版本,每次釋放寫鎖會版本位置會加1 - 寫鎖每次獲取state會加128,釋放也會加128,讀鎖是加減1
- StampedLock 的連續多個讀鎖執行緒,只有第一個是在佇列上,後面的讀執行緒都存在第一個執行緒的cowait棧結構上
- StampedLock 喚醒一個讀鎖執行緒後,讀鎖執行緒會喚醒所有在它
cowait
棧上的等待讀鎖執行緒 - StampedLock 用到了大量的自旋操作,適合持有鎖時間比較短的任務,持有鎖時間長的話等待的執行緒自旋後還是會阻塞自己。
- StampedLock 同一個執行緒先獲取讀鎖,再獲取寫鎖也會死鎖
- StampedLock 寫鎖不支援重入,讀鎖支援重入
- StampedLock 不支援條件鎖
- StampedLock 不支援公平鎖,上來有條件就 CAS 嘗試獲得鎖
StampedLock 與 ReentrantReadWriteLock的區別總結
使用功能上的區別:
- StampedLock 支援樂觀讀鎖,RRWL 沒有
- StampedLock 支援鎖轉換,tryConvertToXXXXX(stamp)
- StampedLock 寫鎖不支援重入,RRWL 支援重入
- StampedLock 不支援條件鎖,RRWL 支援條件鎖
- StampedLock 不支援公平鎖,RRWL 支援公平鎖
底層實現的區別:
- StampedLock 沒有使用同步框架AQS,RRWL 是基於AQS 來實現排隊、阻塞、喚醒等功能的
- StampedLock 獲取鎖時,會直接使用CAS嘗試獲得鎖(不公平,不看排隊),會根據CPU核心數來決定自旋次數等待獲取鎖
- StampedLock 的 CLH 佇列中連續的讀執行緒只有首個節點儲存在佇列中,後面的節點都儲存的首個節點的cowait棧中,即 1→5→4→3→2→1 這種順序。
- StampedLock 中同步狀態 state 被分成了三部分,第8位記錄的是寫鎖的狀態,低7位記錄讀鎖的次數,其他位記錄的是寫鎖的版本
- RRWL 中同步狀態 state 被分成兩部分,高16位記錄讀鎖次數,低16位記錄寫鎖次數
- StampedLock 喚醒一個讀鎖執行緒後,讀執行緒會喚醒所有在它
cowait
棧上的等待讀鎖執行緒