什麼是AQS
字面上來看,AQS
是jdk1.5加入的java.util.concurrent.locks.AbstractQueuedSynchronizer
類,類名翻譯成中文就是抽象的隊列同步器
。由大名鼎鼎的Doug Lea
李大爺來操刀設計並開發實現。
它提供了一種實現阻塞鎖和一系列依賴FIFO
等待隊列的同步器的框架,ReentrantLock
、Semaphore
、CountDownLatch
、CyclicBarrier
等併發類均是基於AQS
來實現的,具體用法是通過繼承AQS
實現其模板方法,然後將子類作爲同步組件的內部類。
爲何要了解AQS
因爲AQS是實現 Lock 的基礎
。想要深入瞭解Java的併發編程,AQS是鎖的實現根基。
AQS原理
AQS核心思想是,如果被請求的共享資源空閒,那麼就將當前請求資源的線程設置爲有效的工作線程,將共享資源設置爲鎖定狀態;如果共享資源被佔用,就需要一定的阻塞等待喚醒機制來保證鎖分配。
圖:AQS原理圖
手動實現AQS
首先,我們模擬一個在線電商的秒殺場景。多位用戶一起來搶購某件商品,看不加鎖時,會不會發生超賣現象。 然後,基於AQS原理,我們實現一個AQS,看加鎖之後,能否解決問題。
模擬秒殺場景
import lombok.extern.slf4j.Slf4j;
/**
* 程序入口
* created at 2020-06-27 20:00
* @author lerry
*/
@Slf4j
public class DiyAqsDemo {
/**
* 剩餘庫存
*/
private volatile int stock = 5;
/**
* 模擬用戶個數
*/
public static final long USER_COUNT = 100;
public static void main(String[] args) {
DiyAqsDemo diyAqsDemo = new DiyAqsDemo();
for (int i = 0; i < USER_COUNT; i++) {
Thread thread = new Thread(() -> diyAqsDemo.buy(), String.format("第%d位顧客的線程", i + 1));
thread.start();
}
}
/**
* 購買
*/
public void buy() {
try {
// 模擬購買的耗時
Thread.sleep(10);
}
catch (InterruptedException e) {
e.printStackTrace();
}
if (stock > 0) {
log.info("購買成功,剩餘庫存爲:{}", this.stock);
stock--;
}
else {
log.info("購買失敗,庫存不足,剩餘庫存爲:{}", this.stock);
}
}
}
截取部分運行結果如下
2020-07-02 06:52:22.392 [第70位顧客的線程] INFO com.hua.threadtest.aqs.DiyAqsDemo - 購買成功,剩餘庫存爲:5
2020-07-02 06:52:22.392 [第71位顧客的線程] INFO com.hua.threadtest.aqs.DiyAqsDemo - 購買成功,剩餘庫存爲:5
2020-07-02 06:52:22.392 [第73位顧客的線程] INFO com.hua.threadtest.aqs.DiyAqsDemo - 購買成功,剩餘庫存爲:5
……
2020-07-02 06:52:22.394 [第84位顧客的線程] INFO com.hua.threadtest.aqs.DiyAqsDemo - 購買成功,剩餘庫存爲:3
2020-07-02 06:52:22.397 [第98位顧客的線程] INFO com.hua.threadtest.aqs.DiyAqsDemo - 購買失敗,庫存不足,剩餘庫存爲:0
2020-07-02 06:52:22.399 [第99位顧客的線程] INFO com.hua.threadtest.aqs.DiyAqsDemo - 購買失敗,庫存不足,剩餘庫存爲:-28
可以發現,第99位顧客來購買時,庫存是負的。雖然我們使用了volatile
關鍵字來修飾庫存變量,但是主內存與工作內存交互時的lock、unlock、read、load、use、assign、store、write
步驟,保證不了原子性,讀取每個線程拷貝了主內存的庫存值到自己的工作內存,它們認爲還有庫存,繼續購買,於是發生了超賣。
圖:主內存和工作內存的交互
那麼,我們實現一個AQS鎖,在判斷庫存是否充足時,加鎖,等庫存修改後,再釋放鎖,不就解決問題了麼。說幹就幹:
手動實現AQS
流程圖如下:
圖:Thread1線程獲取鎖
這時,Thread1嘗試獲取鎖,隊列爲空,獲取鎖的動作,需要是原子的。這裏採用sun.misc.Unsafe
的compareAndSwapInt(Object var1, long var2, int var4, int var5)
函數,來保證原子性。
線程1修改state=1
後,lockHolder引用指向線程1
,程序獲取鎖成功,退出lock()
方法,繼續業務邏輯。
業務邏輯執行完成後,執行unlock()
方法。首先檢查當前線程是不是lockHolder
指向的線程,其他線程是無權限釋放鎖的。 修改state=0
,然後把lockHolder
對象置空。如果等待隊列有值,則取棧首的對象出來,然後喚醒該線程。如果等待隊列沒有對象,則不作處理。
圖:Thread1釋放鎖
源碼
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.LockSupport;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import sun.misc.Unsafe;
/**
* 自定義AQS
* created at 2020-06-27 19:55
* @author lerry
*/
@Slf4j
public class DiyAqsLock {
/**
* <pre>
* 使用一個Volatile的int類型的成員變量來表示同步狀態
* 記錄鎖的狀態 0表示沒有線程持有鎖
* >0表示有線程持有鎖
* </pre>
*/
private volatile int state = 0;
/**
* 用於記錄持有鎖的線程
*/
private Thread lockHolder;
/**
* 存放獲取鎖失敗的線程對象
*/
private ConcurrentLinkedQueue<Thread> waiters = new ConcurrentLinkedQueue<>();
/**
* 通過Unsafe進行cas操作
*/
private static final Unsafe unsafe = UnsafeInstance.getInstance();
private static long stateOffset;
static {
try {
stateOffset = unsafe.objectFieldOffset(DiyAqsLock.class.getDeclaredField("state"));
}
catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
/**
* 加鎖
*/
public void lock() {
// 同步獲取鎖
if (acquire()) {
return;
}
Thread current = Thread.currentThread();
log.debug("線程狀態爲:{}", current.getState());
// 獲取鎖失敗的 添加進隊列裏
waiters.add(current);
// 自旋獲取鎖
for (; ; ) {
// 如果當前線程是棧首的對象,並且獲取鎖成功,則在等待隊列中移除棧首對象,否則繼續等待
if (current == waiters.peek() && acquire()) {
// 移除隊列
waiters.poll();
return;
}
// 讓出cpu的使用權
LockSupport.park(current);
}
}
/**
* 獲取鎖
* @return
*/
private boolean acquire() {
int state = getState();
Thread current = Thread.currentThread();
boolean waitCondition = waiters.size() == 0 || current == waiters.peek();
if (state == 0 && waitCondition) {
// 沒有線程獲取到鎖
if (compareAndSwapState(0, 1)) {
log.info("獲取鎖成功");
// 同步修改成功 將線程持有者修改爲當前線程
setLockHolder(current);
return true;
}
}
return false;
}
/**
* cas操作
* @param expect
* @param update
* @return
*/
public final boolean compareAndSwapState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
/**
* 解鎖
*/
public void unlock() {
System.out.printf("當前等待隊列爲:%s\n", waiters.stream().map(w -> w.getName()).collect(Collectors.toList()));
// 1.校驗釋放鎖的線程是不是當前持有鎖的線程
if (Thread.currentThread() != lockHolder) {
throw new RuntimeException("threadHolder is not current thread");
}
// 2. 釋放鎖修改state
if (getState() == 1 && compareAndSwapState(1, 0)) {
log.info("釋放鎖成功");
// 將鎖的持有線程置爲空
setLockHolder(null);
// 2.喚醒隊列裏的第一個線程
Thread first = waiters.peek();
if (first != null) {
// 解除線程的阻塞
LockSupport.unpark(first);
}
}
}
public int getState() {
return state;
}
public void setLockHolder(Thread lockHolder) {
this.lockHolder = lockHolder;
}
}
@Slf4j
class UnsafeInstance {
public static Unsafe getInstance() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
}
源碼關鍵部分解讀
boolean waitCondition = waiters.size() == 0 || current == waiters.peek();
爲何獲取鎖時,要判斷這一句呢?
看流程圖,如果是線程1獲取鎖,此時等待隊列爲空,可以正常獲取鎖,沒有問題。
如果是線程2來獲取鎖,假設隊列不爲空(隊列裏有線程3、線程4等),爲了保證排在隊伍前面的線程2可以獲取到鎖,我們加上了current == waiters.peek()
,這樣就確保了公平性。先入先出。
我們試着去掉這個條件判斷,在釋放鎖時加上當前等待隊列的打印
/**
* 解鎖
*/
public void unlock() {
System.out.printf("當前等待隊列爲:%s\n", waiters.stream().map(w -> w.getName()).collect(Collectors.toList()));
……
然後再次運行程序,結果如下:
圖:插隊的情況
可以看到,此時獲取鎖,本來排在第1位顧客後面的是第6位顧客,卻被第92位顧客插隊了,不是“先來先得”了。
加上waitCondition
判斷後,運行結果如下:
圖:按照排隊順序獲取鎖
可以看到,這次沒有人再插隊了。
public static Unsafe getInstance()
這裏獲取Unsafe
對象,沒有直接new
,因爲這個類比較特殊,Java不建議用戶直接使用。
查看Unsafe.getUnsafe()
源碼:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
當且僅當調用getUnsafe
方法的類爲引導類加載器所加載時才合法,否則拋出SecurityException異常。
public final boolean compareAndSwapState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
cas操作,解決原子操作,確保對state
進行修改是原子性的。
LockSupport.park(current);
當獲取鎖失敗時,我們採用自旋的方式,讓當前線程先等待。如果這裏使用wait
,則在notify
時,我們無法準確喚醒指定的線程。而java.util.concurrent.locks.LockSupport
類,則提供了public static void unpark(Thread thread)
,可以喚醒指定線程。
Unsafe.objectFieldOffset()
//返回對象成員屬性在內存地址相對於此對象的內存地址的偏移量
public native long objectFieldOffset(Field f);
參考
圖靈學院:手寫高併發秒殺場景同步器鎖防超賣,現場壓測
手寫AQS鎖解決秒殺超賣 - 知乎
Java魔法類:Unsafe應用解析 - 美團技術團隊
環境說明
- java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
- OS:
macOS High Sierra 10.13.4
- 日誌:
logback