寫在開頭
今天在牛客上看到了一個帖子,一個網友吐槽美團一面上來就讓手撕同步器,沒整出來,結果面試直接涼涼。
就此聯想到一週前寫的一篇關於AQS知識點解析的博文,當時也曾埋下伏筆說後面會根據AQS的原理實現一個自定義的同步器,那今天就來把這個坑給填上哈。
常用的AQS架構同步器類
自定義同步器實現步驟
在上一篇文章中我們就已經提過了AQS是基於 模版方法模式
的,我們基於此的自定義同步器設計一般需要如下兩步:
1. 使用者繼承 AbstractQueuedSynchronizer 並重寫指定的方法;
2. 將 AQS 組合在自定義同步元件的實現中,並呼叫其模板方法,而這些模板方法會呼叫使用者重寫的方法。
在模版方法模式下,有個很重要的東西,那就是“鉤子方法”
,這是一種抽象類中的方法,一般使用 protected 關鍵字修飾,可以給與預設實現,空方法居多,其內容邏輯由子類實現,為什麼不適用抽象方法呢?因為,抽象方法需要子類全部實現,增加大量程式碼冗餘!
Ok,有了這層理論知識,我們去看看Java中根據AQS實現的同步工具類有哪些吧
Semaphore(訊號量)
在前面我們講過的synchronized 和 ReentrantLock 都是一次只允許一個執行緒訪問某個資源,而Semaphore(訊號量)可以用來控制同時訪問特定資源的執行緒數量,它並不能保證執行緒安全。
我們下面來看一個關於Semaphore的使用示例:
【程式碼示例1】
public class Test {
private final Semaphore semaphore;
/**
* 構造方法初始化訊號量
* @param limit
*/
public Test(int limit) {
this.semaphore = new Semaphore(limit);
}
public void useResource() {
try {
semaphore.acquire();
// 使用資源
System.out.println("資源use:" + Thread.currentThread().getName());
Thread.sleep(1000); // 模擬資源使用時間
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println("資源release:" + Thread.currentThread().getName());
}
}
public static void main(String[] args) {
// 限制3個執行緒同時訪問資源
Test pool = new Test(3);
for (int i = 0; i < 4; i++) {
new Thread(pool::useResource).start();
}
}
}
輸出:
資源use:Thread-1
資源use:Thread-0
資源use:Thread-2
資源release:Thread-0
資源release:Thread-1
資源release:Thread-2
資源use:Thread-3
資源release:Thread-3
由此結果可看出,我們成功的將同時訪問共享資源的執行緒數限制在了不超過3個的級別,這裡面涉及到了Semaphore的兩個主要方法:acquire()和release()
① acquire():獲取許可
跟進這個方法後,我們會發現其內部呼叫了AQS的一個final 方法acquireSharedInterruptibly(),這個方法中又呼叫了tryAcquireShared(arg)放,作為AQS中的鉤子方法,這個方法的實現在Semaphore的兩個靜態內部類 FairSync(公平模式) 和 NonfairSync(非公平模式) 中。
② release():釋放許可
同樣跟入這個方法,裡面用了AQS的releaseShared(),而在這個方法內也毫無疑問的用了tryReleaseShared(int arg)這個鉤子方法,原理同上,不再冗釋。
【補充】
此外,在Semaphore中還有一個Sync的內部類,提供nonfairTryAcquireShared()自旋獲取資源,以及tryReleaseShared(int releases),共享方式嘗試釋放資源。
除了Semaphore(訊號量)外,基於AQS實現的還有CountDownLatch (倒數計時器)、CyclicBarrier(迴圈柵欄),本來想在一篇文章中講完的,但感覺篇幅上會非常長,遂放棄,後面分篇學習吧。
手寫一個同步器!
好了,有了上面的一系列學習,我們現在來手撕一個自定義的同步器吧,原理都一樣滴,開始前,先貼上AQS中的幾個鉤子方法,防止待會忘記,哈哈!
【鉤子方法】
//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
protected boolean tryAcquire(int)
//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
protected boolean tryRelease(int)
//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
protected int tryAcquireShared(int)
//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。
protected boolean tryReleaseShared(int)
//該執行緒是否正在獨佔資源。只有用到condition才需要去實現它。
protected boolean isHeldExclusively()
寫一個基於AQS的互斥鎖,統一時刻只允許一個執行緒獲取資源。
步驟一:
首先,我們在第一步,我們定義一個互斥鎖類OnlySyncByAQS,在類中我們同樣寫一個靜態內部類去繼承AbstractQueuedSynchronizer,在內部類中,我們重寫AQS的tryAcquire方法,獨佔方式,嘗試獲取資源;重寫tryRelease()嘗試釋放資源,這倆為主要方法!
然後我們再進一步封裝成lock()與unlock()的上鎖與解鎖方法,並在裡面透過模版方法模式,去呼叫AQS中的acquire()和release(),從而去調到我們對模版方法的實現。
【程式碼示例2】
public class OnlySyncByAQS {
private final Sync sync = new Sync();
/**
* 獲取許可,給資源上鎖
*/
public void lock() {
sync.acquire(1);
}
/**
* 釋放許可,解鎖
*/
public void unlock() {
sync.release(1);
}
/**
* 判斷是否獨佔
* @return
*/
public boolean isLocked() {
return sync.isHeldExclusively();
}
/**
* 靜態內部類,繼承AQS,重寫鉤子方法
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 重寫AQS的tryAcquire方法,獨佔方式,嘗試獲取資源。
*/
@Override
protected boolean tryAcquire(int arg) {
//CAS 嘗試更改狀態
if (compareAndSetState(0, 1)) {
//獨佔模式下,設定鎖的持有者為當前執行緒,來自於AOS
setExclusiveOwnerThread(Thread.currentThread());
System.out.println(Thread.currentThread().getName()+"獲取鎖成功");
return true;
}
System.out.println(Thread.currentThread().getName()+"獲取鎖失敗");
return false;
}
/**
* 獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
* @param arg
* @return
*/
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
//置空鎖的持有者
setExclusiveOwnerThread(null);
//改狀態為0,未鎖定狀態
setState(0);
System.out.println(Thread.currentThread().getName()+"釋放鎖成功!");
return true;
}
/**
* 判斷該執行緒是否正在獨佔資源,返回state=1
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
}
步驟二:
第二步,我們寫一個測試類去呼叫這個自定義的互斥鎖。
【程式碼示例2】
public class Test {
private OnlySyncByAQS onlySyncByAQS = new OnlySyncByAQS();
public void use(){
onlySyncByAQS.lock();
try {
//休眠1秒獲取使用共享資源
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
onlySyncByAQS.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
//多執行緒競爭資源,每次僅一個執行緒拿到鎖
for (int i = 0; i < 3; i++) {
new Thread(()->{
test.use();
}).start();
}
}
}
輸出:
Thread-0獲取鎖成功
Thread-1獲取鎖失敗
Thread-2獲取鎖失敗
Thread-1獲取鎖失敗
Thread-1獲取鎖失敗
Thread-0釋放鎖成功!
Thread-1獲取鎖成功
Thread-1釋放鎖成功!
Thread-2獲取鎖成功
Thread-2釋放鎖成功!
由輸出結果可以看出作為互斥鎖,每次僅一個執行緒可以獲取到鎖資源,其他執行緒會不斷嘗試獲取並失敗,直至該執行緒釋放鎖資源!
結尾彩蛋
如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!