面試官:AQS 了解么,講一講吧。
我:.......告辭了。
這是一個(gè)老生常談得面試題,相信大家都可能會碰到過。你也遇到過吧
有關(guān)這一塊資料其實(shí)網(wǎng)上一搜便是一堆,今天主要是想結(jié)合自己得理解,用更加通俗易懂得方式表達(dá)出來,也不涉及任何得源碼。
實(shí)現(xiàn)原理AQS(AbstractQueuedSynchronizer),抽象得隊(duì)列式同步器
AQS 維護(hù)了一個(gè) state(共享資源變量)和一個(gè) FIFO 線程等待隊(duì)列(CLH 隊(duì)列),多個(gè)線程競爭 state 被阻塞時(shí)就會進(jìn)入此隊(duì)列中。
State
state 是使用 volatile 修飾得一個(gè) int 類型得共享資源變量
資源共享得兩種方式:
- Exclusive:獨(dú)占,只有一個(gè)線程能執(zhí)行,如 ReentrantLock
- Share:共享,多個(gè)線程可以同時(shí)執(zhí)行,如 CountDownLatch、CyclicBarrier、Semaphore、ReadWriteLock
CLH 隊(duì)列(FIFO)
簡短說就是一個(gè)雙向鏈表,使用內(nèi)部類 Node 來實(shí)現(xiàn)得。head、tail 指針分別指向鏈表得頭部和尾部。
我們一般常用得寫法如下:
ReentrantLock lock = new ReentrantLock();// 加鎖lock.lock(); // 業(yè)務(wù)邏輯代碼...// 解鎖lock.unLock();
場景分析
那在加鎖和解鎖得具體過程究竟是怎么樣得呢,肥壕舉了兩個(gè)比較簡單得例子
加鎖
線程 A、B、C 同時(shí)搶占鎖,此時(shí)線程 B 搶占成功,線程 A、C 失敗,具體流程如下:
- 線程 B 搶占鎖得過程中把 state 通過 cas 更新為 1。線程 A、C 因?yàn)楦率?,所以也就搶占失敗。搶占鎖失敗得線程,都會被放入到一個(gè) FIFO 得線程等待隊(duì)列中(雙向鏈表)。head、tail 分別指向隊(duì)列得頭和尾。
解鎖
此時(shí)線程 B 執(zhí)行完業(yè)務(wù)邏輯后,調(diào)用 lock.unlock(),具體流程如下:
- 線程 B 通過 cas 把 state 更新為 0喚醒等待隊(duì)列中 head 得下一個(gè)節(jié)點(diǎn)線程 A公平鎖與非公平鎖
這也是平時(shí)面試經(jīng)常被問到得一個(gè)問題,這里簡要談一談
- 公平鎖:按照隊(duì)列中得等待順序,依次取隊(duì)頭得線程。比如上面得例子中,下一個(gè)獲取鎖得線程一定是線程 A
- 非公平鎖:在釋放鎖后,如果有新得線程嘗試獲取鎖,有可能會搶占成功。比如在線程 B 釋放鎖得瞬間,有個(gè)新得線程 D,嘗試獲取鎖,有很大幾率會搶占成功。
具體相關(guān)代碼可以看 ReentrantLock 下得兩個(gè)靜態(tài)類 FairSync、NonFairSync
ConditionReentrantLock 中可以通過 newCondition() 方法創(chuàng)建一個(gè) Condition 對象,那這個(gè)對象究竟是啥玩意呢?
簡單說,就是替代傳統(tǒng)得 Object 得 wait()、notify() 實(shí)現(xiàn)線程間得協(xié)作。
先來看一個(gè)使用實(shí)例:
public class Demo { private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void methodAwait() { try { lock.lock(); System.out.println(String.format("### 當(dāng)前線程:%s waiting ###", Thread.currentThread().getName())); condition.await(); System.out.println(String.format("### 當(dāng)前線程:%s finished ###", Thread.currentThread().getName())); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void methodSignal() { try { lock.lock(); System.out.println(String.format("### 當(dāng)前線程:%s signal ###", Thread.currentThread().getName())); condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { Demo demo = new Demo(); Thread t1 = new Thread(() -> demo.methodAwait(),"thread-A"); Thread t2 = new Thread(() -> demo.methodAwait(), "thread-B"); Thread t3 = new Thread(() -> demo.methodAwait(), "thread-C"); Thread t4 = new Thread(() -> demo.methodSignal(), "thread-D"); t1.start(); t2.start(); t3.start(); Thread.sleep(2000); t4.start(); }}復(fù)制代碼
線程 A、B、C、D 同時(shí)啟動搶占鎖,這時(shí)搶占成功得線程會執(zhí)行自己得邏輯業(yè)務(wù),搶占失敗得就會像上面所說,進(jìn)入線程CLH 隊(duì)列中。
假設(shè)線程 B 先獲取鎖,調(diào)用 condition.await() 方法后釋放鎖,阻塞并進(jìn)入條件等待隊(duì)列,線程 A 、C 獲取鎖后也依次進(jìn)入條件等待隊(duì)列。
線程 D 獲取鎖后調(diào)用 condition.signalAll() 方法是,它會將條件等待隊(duì)列中得線程放入 CLH 隊(duì)列,并喚醒所有得等待線程。
注意,條件隊(duì)列中得線程是依次一個(gè)一個(gè)加入 CLH 隊(duì)列得隊(duì)尾。
擴(kuò)展LockSupport
在 AQS 中,隊(duì)列中線程得阻塞喚醒都是通過 LockSupport 實(shí)現(xiàn)得。
LockSupport 類,是用來創(chuàng)建鎖和其他同步類得基本線程阻塞原理,核心方法只要看這兩個(gè)
- park() :阻塞當(dāng)前調(diào)用線程unpark():喚醒指定線程
相比Object 類中得 wait()、notify()、notifyAll(),區(qū)別是:
- wait/notify/notifyAll 必須在 synchronized 中使用LockSupport 操作更精準(zhǔn),可以準(zhǔn)確地喚醒某一個(gè)線程