引語
我們會經常見到像ReentrantReadWriteLock、ReentrantLock這樣的鎖,在JUC中Lock接口和AQS(AbstractQueuedSynchronizer)非常重要。
這裏會設計到CAS,如果沒有CAS操作的基礎,建議先學習CAS操作。
Lock接口相關要實現API
AQS提供的模板方法:
Lock和AQS的關係
當我們要實現一個Lock接口的實現類時,需要依賴一個AQS(隊列同步器的),因爲隊列同步器幫我們維護是線程阻塞和釋放的邏輯,比如,線程競爭鎖時,當一個線程沒有競爭到時,要把它丟進一個FIFO的隊列,當獲取到鎖的線程釋放鎖時,要通知在隊列裏的線程出來競爭。
如果讓你自己維護這種競爭的邏輯,會很麻煩。所以JDK給我們提供了AQS,暴露出來了一些底層的API,讓我們去覆蓋,然後有一些模板API來調用我們底層的API,並且維護好隊列。這樣我們就只用把精力放在獲得到鎖和沒有獲得到鎖的邏輯上了。不用管沒有線程獲得鎖後以及線程釋放鎖後如何通知其他線程怎麼維護。
Lock和AQS的調用關係大致如下。
自定義的鎖MyLock實現Lock接口,然後重寫了Lock接口的方法。
自定義的MyAQS繼承AQS類,然後我們重寫tryAcquire、tryRelease和isHeldExclusively等方法。
紅色箭頭的起始點其實就是AQS提供給我們的模板API,比如我們重寫的tryAcquire、tryRelease和isHeldExclusively,我們要實現的API只需要調用AQS提供給我們的state相關API(綠色箭頭尾部)來維護是否獲取到鎖就可以了。
MyLock的API調用AQS的模板方法(右邊藍色箭頭尾部),也可以調用MyAQS重寫的方法(左邊藍色箭頭尾部)來實現加鎖、釋放鎖。
AQS模板API的對應關係
綠色箭頭表示獨佔性鎖的實現邏輯,紅色箭頭表示共享式鎖實現的邏輯。
所謂的獨佔性鎖意思就是,只要有一個線程拿到鎖,其他線程全部T出去到隊列等待。
共享性鎖就好理解了,一部分個性化(根據tryAcquire返回值決定)的線程可以拿到鎖,沒拿到的到隊列。
自定義獨佔鎖
package com.anyco.aqs;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class OnlyOneLock implements Lock {
private final OnlyOneLockAQS aqs=new OnlyOneLockAQS();
private static class OnlyOneLockAQS extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
//狀態爲0時,設置爲1,表示當前線程獲取到鎖
if(compareAndSetState(0,1)){
//設置當前線程爲獲取到鎖的線程
setExclusiveOwnerThread(Thread.currentThread());
return true ;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
//如果狀態爲0,表示還沒有線程獲取到鎖,就沒有釋放鎖的可能
if(getState()==0) {throw new IllegalMonitorStateException();}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
//判斷該線程是否被佔有
return getState()==1; //state爲表示鎖被佔有
}
}
@Override
public void lock() {
aqs.acquire(1); //獲取鎖成功時,返回,沒有成功則進入隊列等待,會調用OnlyOneLockAQS重寫的tryAcquire
}
@Override
public void lockInterruptibly() throws InterruptedException {
aqs.acquireInterruptibly(1); //帶中斷檢測的獲取
}
@Override
public boolean tryLock() {
return aqs.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return aqs.tryAcquireNanos(1,unit.toNanos(time)); //帶超時限制的獲取鎖,指定時間內沒有獲取達到則返回false
}
@Override
public void unlock() {
aqs.release(1);
}
@Override
public Condition newCondition() {
return null;
}
}
OnlyOneLock中的實現API都是調用了同步器AQS的模板方法和我們實現的方法來實現鎖的邏輯的。
我們實現的try開頭的那幾個API只需要管是否獲取到鎖的邏輯。
這裏強調一下,其實在Lock接口中只有tryLock()這個API會直接調用tryAcquire()這個我們實現的API之外,其他的API其實都是調用的AQS的模板方法,因爲模板方法封裝了很多複雜的隊列通知等邏輯。
自定義共享鎖
寫共享鎖之前,可以想象一個場景。大家應該都知道限流,現在假如有個需求,一個應用級的限流(業務接口層面的),要求一個接口最後只能被5個線程併發訪問,後續的線程再訪問,直接返回,不做業務邏輯處理.(常用語秒殺業務中某個商品只有5個,那麼有必要放很多請求進來嗎?)
public class ShareLock implements Lock {
private ShareLockAQS aqs=new ShareLockAQS(5);
private static class ShareLockAQS extends AbstractQueuedSynchronizer {
protected ShareLockAQS(Integer count) { //count表示鎖的總數
super();
setState(count);
}
@Override
protected int tryAcquireShared(int arg) { //arg表示請求的鎖的數量
for (; ; ) { //for循環
Integer state = getState();
Integer newCount = state - arg; //剩餘的可獲取鎖的數量需要減少
if (newCount < 0 || compareAndSetState(state, newCount)) { //CAS操作,更新剩餘的可獲取鎖的數量
return newCount; //剩餘的可獲取鎖的數量
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
for (; ; ) {
//注意這裏不能直接setState了,因爲可能多個線程同時release
Integer state = getState();
Integer newCount = state + arg;
if (compareAndSetState(state,newCount)) { //CAS操作,更新剩餘的可獲取的鎖數量
return true;
}
}
}
@Override
protected boolean isHeldExclusively() {
return getState()==0;
}
}
@Override
public void lock() {
aqs.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
aqs.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return aqs.tryAcquireShared(1)>=0;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return aqs.tryAcquireSharedNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
aqs.releaseShared(1);
}
@Override
public Condition newCondition() {
return null;
}
}
內部維護了一個state,初始化爲5,獲取一個鎖,減1,釋放一個鎖,+1。獲取鎖和釋放鎖的兩個方法我們都用到了for循環。在這裏需要強調,務必使釋放鎖最終成功。
使用ShareLock 來測試
上面我們自定義了一個共享鎖ShareLock ,這把鎖我們定義的是同一時間只有5個線程可以持有。接下來我們使用這個自定義鎖,然後啓動10個線程去獲取鎖。
public class TestShareLock {
private static ShareLock shareLock = new ShareLock(); //new一個共享鎖,只能有5個線程可以同時持有
public static void main(String[] args) {
for(int i=0; i<10; i++) { //啓動10個線程去獲取鎖
Thread t = new Thread(new MyLockThread(shareLock));
t.start();
}
}
}
class MyLockThread implements Runnable {
private ShareLock shareLock;
public MyLockThread(ShareLock shareLock) {
this.shareLock = shareLock;
}
@Override
public void run() {
boolean succ = shareLock.tryLock(); //請求鎖
System.out.println(succ);
}
}
執行結果:
true
true
true
true
true
false
false
false
false
false
很明顯,如果前面5個線程不釋放鎖,後面的線程都是沒有辦法獲取鎖的。
這和我們剛剛講到的限流有何關係呢?如果我們一個接口最多隻能被5個線程併發訪問,訪問成功就是獲取鎖成功了,後續的線程再訪問因爲無法獲取鎖,所有會訪問失敗,這樣便起到了限流的作用,當然我們還可以在這裏加釋放鎖的邏輯,如果獲取鎖的線程操作完畢,我們可以讓這個線程釋放鎖,從而讓其他的線程能夠獲取鎖。
參考:
https://juejin.im/post/5a3a09d9f265da4312810fb9
https://juejin.im/post/5a3c6aa551882538d3101d5f