我手寫了AQS實現、畫了3張流程圖,就是爲了讓你徹底搞明白AQS原理

什麼是AQS

字面上來看,AQS是jdk1.5加入的java.util.concurrent.locks.AbstractQueuedSynchronizer類,類名翻譯成中文就是抽象的隊列同步器。由大名鼎鼎的Doug Lea李大爺來操刀設計並開發實現。
它提供了一種實現阻塞鎖和一系列依賴FIFO等待隊列的同步器的框架,ReentrantLockSemaphoreCountDownLatchCyclicBarrier等併發類均是基於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.UnsafecompareAndSwapInt(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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章