作者:小傅哥
部落格:https://bugstack.cn
Github:https://github.com/fuzhengwei/CodeGuide/wiki
沉澱、分享、成長,讓自己和他人都能有所收穫!?
一、前言
學Java怎麼能,突飛猛進的成長?
是不是你看見過的突飛猛進都是別人,但自己卻很難!
其實並沒有一天的突飛猛進,也沒有一口吃出來的胖子。有得更多的時候日積月累、不斷沉澱,最後才能爆發、破局!
舉個簡單的例子,如果你大學畢業時候已經寫了40萬行程式碼,還找不到工作嗎?但40萬行平均到每天並不會很多,重要的是持之以恆的堅持。
二、面試題
謝飛機,小記!
東風吹、戰鼓擂,不加班、誰怕誰!哈哈哈,找我大哥去。
謝飛機:喂,大哥。我女友面試卡住了,強人鎖
難,鎖我也不會!
面試官:你不應該不會呀,問你一個,基於 AQS 實現的鎖都有哪些?
謝飛機:嗯,有 ReentrantLock...
面試官:還有呢?
謝飛機:好像想不起來了,sync也不是!
面試官:哎,學點漏點,不思考、不總結、不記錄。你這樣人家面試你就沒法聊了,最起碼你要有點深度。
謝飛機:嘿嘿,記住了。來我家吃火鍋吧,細聊。
三、共享鎖 和 AQS
1. 基於 AQS 實現的鎖有哪些?
AQS(AbstractQueuedSynchronizer),是 Java 併發包中非常重要的一個類,大部分鎖的實現也是基於 AQS 實現的,包括:
ReentrantLock
,可重入鎖。這個是我們最開始介紹的鎖,也是最常用的鎖。通常會與 synchronized 做比較使用。ReentrantReadWriteLock
,讀寫鎖。讀鎖是共享鎖、寫鎖是獨佔鎖。Semaphore
,訊號量鎖。主要用於控制流量,比如:資料庫連線池給你分配10個連結,那麼讓你來一個連一個,連到10個還沒有人釋放,那你就等等。CountDownLatch
,閉鎖。Latch 門閂的意思,比如:說四個人一個漂流艇,坐滿了就推下水。
這一章節我們主要來介紹 Semaphore ,訊號量鎖的實現,其實也就是介紹一個關於共享鎖的使用和原始碼分析。
2. Semaphore 共享鎖使用
Semaphore semaphore = new Semaphore(2, false); // 建構函式入參,permits:訊號量、fair:公平鎖/非公平鎖
for (int i = 0; i < 8; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "蹲坑");
Thread.sleep(1000L);
} catch (InterruptedException ignore) {
} finally {
semaphore.release();
}
}, "蹲坑編號:" + i).start();
}
這裡我們模擬了一個在高速服務區,廁所排隊蹲坑的場景。由於坑位有限,為了避免造成擁擠和踩踏,保安人員在門口攔著,感覺差不多,一次釋放兩個進去,一直到都釋放。你也可以想成早上坐地鐵上班,或者旺季去公園,都是一批一批的放行
測試結果
蹲坑編號:0蹲坑
蹲坑編號:1蹲坑
蹲坑編號:2蹲坑
蹲坑編號:3蹲坑
蹲坑編號:4蹲坑
蹲坑編號:5蹲坑
蹲坑編號:6蹲坑
蹲坑編號:7蹲坑
Process finished with exit code 0
- Semaphore 的建構函式可以傳遞是公平鎖還是非公平鎖,最終的測試結果也不同,可以自行嘗試。
- 測試執行時,會先輸出
0坑、1坑
,之後2坑、3坑
...,每次都是這樣兩個,兩個的釋放。這就是 Semaphore 訊號量鎖的作用。
3. Semaphore 原始碼分析
3.1 建構函式
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
permits:n. 許可證,特許證(尤指限期的)
預設情況下只需要傳入 permits 許可證數量即可,也就是一次允許放行幾個執行緒。建構函式會建立非公平鎖。如果你需要使用 Semaphore 共享鎖中的公平鎖,那麼可以傳入第二個建構函式的引數 fair = false/true。true:FairSync,公平鎖。在我們前面的章節已經介紹了公平鎖相關內容和實現,以及CLH、MCS 《公平鎖介紹》
初始許可證
數量
FairSync/NonfairSync(int permits) {
super(permits);
}
Sync(int permits) {
setState(permits);
}
protected final void setState(int newState) {
state = newState;
}
在建構函式初始化的時候,無論是公平鎖還是非公平鎖,都會設定 AQS 中 state 數量值。這個值也就是為了下文中可以獲取的訊號量扣減和增加的值。
3.2 acquire 獲取訊號量
方法 | 描述 |
---|---|
semaphore.acquire() |
一次獲取一個訊號量,響應中斷 |
semaphore.acquire(2) |
一次獲取n個訊號量,響應中斷(一次佔2個坑) |
semaphore.acquireUninterruptibly() |
一次獲取一個訊號量,不響應中斷 |
semaphore.acquireUninterruptibly(2) |
一次獲取n個訊號量,不響應中斷 |
- 其實獲取訊號量的這四個方法,主要就是,一次獲取幾個和是否響應中斷的組合。
semaphore.acquire()
,原始碼中實際呼叫的方法是,sync.acquireSharedInterruptibly(1)
。也就是相應中斷,一次只佔一個坑。semaphore.acquire(2)
,同理這個就是一次要佔兩個名額,也就是許可證。生活中的場景就是我給我朋友排的對,她來了,進來吧。
3.3 acquire 釋放訊號量
方法 | 描述 |
---|---|
semaphore.release() |
一次釋放一個訊號量 |
semaphore.release(2) |
一次獲取n個訊號量 |
有獲取就得有釋放,獲取了幾個訊號量就要釋放幾個訊號量。當然你可以嘗試一下,獲取訊號量 semaphore.acquire(2) 兩個,釋放訊號量 semaphore.release(1),看看執行效果
3.4 公平鎖實現
訊號量獲取過程,一直到公平鎖實現。semaphore.acquire
-> sync.acquireSharedInterruptibly(permits)
-> tryAcquireShared(arg)
semaphore.acquire(1);
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
FairSync.tryAcquireShared
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
hasQueuedPredecessors
,公平鎖的主要實現邏輯都在於這個方法的使用。它的目的就是判斷有執行緒排在自己前面沒,以及把執行緒新增到佇列中的邏輯實現。在前面我們介紹過CLH等實現,可以往前一章節閱讀for (;;)
,是一個自旋的過程,通過 CAS 來設定 state 偏移量對應值。這樣就可以避免多執行緒下競爭獲取訊號量衝突。getState()
,在建構函式中已經初始化 state 值,在這裡獲取訊號量時就是使用 CAS 不斷的扣減。- 另外需要注意,共享鎖和獨佔鎖在這裡是有區別的,獨佔鎖直接返回true/false,共享鎖返回的是int值。
- 如果該值小於0,則當前執行緒獲取共享鎖失敗。
- 如果該值大於0,則當前執行緒獲取共享鎖成功,並且接下來其他執行緒嘗試獲取共享鎖的行為很可能成功。
- 如果該值等於0,則當前執行緒獲取共享鎖成功,但是接下來其他執行緒嘗試獲取共享鎖的行為會失敗。
3.5 非公平鎖實現
NonfairSync.nonfairTryAcquireShared
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
- 有了公平鎖的實現,非公平鎖的理解就比較簡單了,只是拿去了
if (hasQueuedPredecessors())
的判斷操作。 - 其他的邏輯實現都和公平鎖一致。
3.6 獲取訊號量失敗,加入同步等待佇列
在公平鎖和非公平鎖的實現中,我們已經看到正常獲取訊號量的邏輯。那麼如果此時不能正常獲取訊號量呢?其實這部分執行緒就需要加入到同步佇列。
doAcquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 首先
doAcquireSharedInterruptibly
方法來自 AQS 的內部方法,與我們在學習競爭鎖時有部分知識點相同,但也有一些差異。比如:addWaiter(Node.SHARED)
,tryAcquireShared
,我們主要介紹下這內容。 Node.SHARED
,其實沒有特殊含義,它只是一個標記作用,用於判斷是否共享。final boolean isShared() { return nextWaiter == SHARED; }
tryAcquireShared
,主要是來自Semaphore
共享鎖中公平鎖和非公平鎖的實現。用來獲取同步狀態。setHeadAndPropagate(node, r)
,如果r > 0,同步成功後則將當前執行緒結點設定為頭結點,同時 helpGC,p.next = null,斷鏈操作。shouldParkAfterFailedAcquire(p, node)
,調整同步佇列中 node 結點的狀態,並判斷是否應該被掛起。這在我們之前關於鎖的文章中已經介紹。parkAndCheckInterrupt()
,判斷是否需要被中斷,如果中斷直接丟擲異常,當前結點請求也就結束。cancelAcquire(node)
,取消該節點的執行緒請求。
4. CountDownLatch 共享鎖使用
CountDownLatch 也是共享鎖的一種型別,之所以在這裡體現下,是因為它和 Semaphore 共享鎖,既相似有不同。
CountDownLatch 更多體現的組團一波的思想,同樣是控制人數,但是需要夠一窩。比如:我們說過的4個人一起上皮划艇、兩個人一起上蹺蹺板、2個人一起蹲坑我沒見過,這樣的方式就是門閂 CountDownLatch 鎖的思想。
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
exec.execute(() -> {
try {
int millis = new Random().nextInt(10000);
System.out.println("等待遊客上船,耗時:" + millis + "(millis)");
Thread.sleep(millis);
} catch (Exception ignore) {
} finally {
latch.countDown(); // 完事一個扣減一個名額
}
});
}
// 等待遊客
latch.await();
System.out.println("船長急躁了,開船!");
// 關閉執行緒池
exec.shutdown();
}
- 這一個公園遊船的場景案例,等待10個乘客上傳,他們比較墨跡。
- 上一個扣減一個
latch.countDown()
- 等待遊客都上船
latch.await()
- 最後船長開船!!
急躁了
測試結果
等待遊客上船,耗時:6689(millis)
等待遊客上船,耗時:2303(millis)
等待遊客上船,耗時:8208(millis)
等待遊客上船,耗時:435(millis)
等待遊客上船,耗時:9489(millis)
等待遊客上船,耗時:4937(millis)
等待遊客上船,耗時:2771(millis)
等待遊客上船,耗時:4823(millis)
等待遊客上船,耗時:1989(millis)
等待遊客上船,耗時:8506(millis)
船長急躁了,開船!
Process finished with exit code 0
- 在你實際的測試中會發現,
船長急躁了,開船!
,會需要等待一段時間。 - 這裡體現的就是門閂的思想,組隊、一波帶走。
- CountDownLatch 的實現與 Semaphore 基本相同、細節略有差異,就不再做原始碼分析了。
四、總結
- 在有了 AQS、CLH、MCS,等相關鎖的知識瞭解後,在學習其他知識點也相對容易。基本以上和前幾章節關於鎖的介紹,也是面試中容易問到的點。可能由於目前分散式開發較多,單機的多執行緒效能壓榨一般較少,但是對這部分知識的瞭解非常重要
- 得益於Lee老爺子的操刀,併發包鎖的設計真的非常優秀。每一處的實現都可以說是精益求精,所以在學習的時候可以把小傅哥的文章當作拋磚,之後繼續深挖設計精髓,不斷深入。
- 共享鎖的使用可能平時並不多,但如果你需要設計一款類似資料庫執行緒池的設計,那麼這樣的訊號量鎖的思想就非常重要了。所以在學習的時候也需要有技術遷移的能,不斷把這些知識複用到實際的業務開發中。