1. Lock接口簡介
Lock接口是Java concurrent包中比較重要的接口。Lock的實現類有ReentrantLock、WriteLock、ReadLock。Lock類中定義了六個方法
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
- lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的。
- unlock()方法是用來釋放鎖的。調用了lock()必須成對的調用unlock(),否則會出現死鎖的情況。
- newCondition()方法是給鎖創建一個ConditionObject對象,ConditionObject的await()方法能讓當前線程釋放鎖,並且讓線程處於阻塞狀態。==signal()方法是通知其他正在該ConditionObject上阻塞的線程加入競爭鎖的隊列中==
鎖分爲排他鎖和共享鎖
- 排他鎖同一時間只能被一個線程獲取到
- 共享鎖可以被多個線程同時獲取
ReentrantLock和WriteLock是排他鎖,ReadLock是共享鎖
2. Lock的使用
通過Lock來實現同步功能,我們一般會瀟灑地寫出以下代碼Lock lock = new ReentrantLock();
try{
lock.lock();//標記1
doSomeThing();
}finally{
lock.unlock();
}
通過lock.lock()方法就能實現代碼的同步。就問你神不神奇。可能大家面試的時候也經常會被問到如何用Lock實現同步,很大一部分同學應該都知道使用lock()方法。正當你洋洋得意,以爲這就是整個答案的時候,面試官也會輕輕點頭露出一份詭異的笑容同時拋出下一個問題,那麼請問lock方法的實現原理是什麼,爲什麼調用這個方法就能實現線程間同步呢?那麼下面由我帶大家進入lock()方法的揭祕之旅
3. lock()方法揭祕
lock()方法是Lock接口中定義的方法。要想探索lock()方法的奧祕,我們得先從Lock接口的實現類中找到一個突破口才行啊。好吧那就從ReentrantLock類開始吧。ReentrantLock的含義是指==重入鎖==,重入鎖的意思是指同一線程可以多次獲取到同一把鎖,每次獲取到鎖,鎖的狀態會加1,每次釋放鎖,鎖狀態會減1。後面我們會講解到重入鎖的實現原理。 我們接着來探索ReentrantLock的lock()方法//ReentrantLock.java
public void lock() {
sync.lock();
}
1.原來只是調用了sync對象的lock()方法,so easy找到sync對象的定義
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}
2.至此我們引出了一個非常重要的類AbstractQueuedSynchronizer。中文意思就是抽象隊列同步器(下文簡稱AQS)。顧名思義隊列同步器,那內部結構肯定是用隊列來實現的。隊列最重要的性質就是先進先出。這個給各位先劇透下,如果一個線程獲取鎖失敗後,會封裝成一個Node節點加入sync的隊列中。注意Sync類是ReentrantLock的靜態內部類,也就意味着一把鎖只有一個sync同步器,一個同步器內部維護了一個雙向列表(也就是隊列)。隊列裏面維護的是獲取鎖失敗的線程組成的節點。當鎖被釋放後,根據先進先出的性質,在隊首的節點的線程將優先有獲取鎖的權利。==當然也有意外== ==當然也有意外== ==當然也有意外== 並不是所有的鎖都是在隊首的節點優先獲取鎖。這種情況就是,有個運氣爆表的線程我們暫且叫它線程T吧,在剛運行到lock.lock()方法的時候,佔有鎖的線程剛好執行完,T去嘗試獲取了下鎖,因爲它運氣爆表嘛,剛好他獲取到了鎖。剛纔我們講了,如果一個線程獲取鎖失敗,它會被封裝成Node加入sync同步器的隊列中,等待被喚醒再次獲取鎖。因爲線程T很幸運在第一時間獲取到了鎖,那麼它根本就不需要到sync隊列中等待。那麼有線程幸運就自然有線程不公平,在sync隊列中苦哈哈排隊的隊首線程本來是鎖釋放後第一個能獲取到鎖的線程。至此我們引出兩個非常重要的概念 ==公平鎖和非公平鎖==。接下來我們去代碼中探究下公平鎖和非公平鎖的實現原理
4. 公平鎖和非公平鎖
公平鎖和非公平鎖的區別就好比大家在食堂排隊打菜。打菜阿姨就是那把鎖。阿姨正在給同學打菜就當是上鎖,阿姨給一個同學打完菜就當做是釋放鎖。阿姨給一個同學打完菜後幹嘛呢,當然是給下一個同學打菜了,排着長隊呢!but 但是這期間是有幾秒鐘到幾十秒中不等的間隔時間哦。那萬一說時遲那時快就在這個時候,有個同學不守規矩想插隊,臭不要臉的把飯盒湊到阿姨手前,而阿姨又是個老實的女子,一個不忍心給插隊同學打了菜(上了鎖)那這個阿姨就是非公平鎖。如果阿姨是個有原則的女子,喝到,這位同學請排隊。那麼這個阿姨就是公平鎖。//公平鎖
static final class FairSync extends Sync {
final void lock() {
acquire(1);//公平鎖沒有插隊現象,正常請求鎖
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
//非公平鎖
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
//非公平鎖if裏面的條件就是插隊,
//compareAndSetState(0, 1)
//意思就是插隊看能不能拿到鎖
//能拿到把當前線程設爲sync的獨佔線程
setExclusiveOwnerThread(Thread.currentThread());
else
//插隊沒競爭過其他同學,被脾氣不好的同學嚇到了
//正常請求鎖
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
大家在看公平鎖和非公平鎖的源碼時一定要對比看他們的區別在哪裏。經過以上對sync的lock()方法講解,想必大家對公平鎖和非公平鎖有了一個基本的認識。至此兩個問題想考考讀者
咱們接着看代碼,仔細對比後發現不管是FairSync還是NonFairSync的lock()方法最終還是會調用acquire(1)方法。那麼acquire(1)方法又是何方神聖呢?
- 線程獲取鎖失敗後他是怎麼進入AQS隊列的
- 非公平鎖只要插隊失敗了就立馬進入AQS隊列嗎
//AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
只有三行的一個方法,可以說是很簡單了。但是卻有大玄機,乍一看easy,深一看有點暈。selfInterrupt()很是迷惑,這不是中斷線程的嗎,既然中斷了那還玩個屁,線程都掛了,上鎖有毛用。筆者剛開始看這段代碼也是很疑惑,但是仔細想想,沒那麼簡單。
首先看tryAcquire(1)方法了。他的具體實現是在Sync子類中
//公平鎖
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//看鎖是否空閒(打菜阿姨是否空閒)
//hasQueuedPredecessors表示是否在隊首(我們還沒講入隊,有個概念就行)
//老實孩子如果不是在隊首,直接走else if
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//如果是在隊首,而且得到了公平阿姨的鎖,設置當前線程爲鎖sync的線程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//已經上鎖了,而且是已經獲取到鎖的線程請求鎖,前面講到的重入鎖,狀態+1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//不是隊首或者是隊首但是或取鎖失失敗或者上鎖情況下,不是獲取鎖的線程,返回false
return false;
}
}
//非公平鎖
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//如果當前鎖空閒狀態
//對比公平鎖哈!!!
//看你是非公平鎖(好說話的阿姨)不管自己在不在隊首,先插個隊如果獲取到了返回true
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//同公平鎖
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果沒上鎖的情況插隊失敗,或者上鎖情況下,不是獲取鎖的線程,返回true
return false;
}
再回看acquire(1)方法
//AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 如果tryAcquire(arg)返回true,後面的判斷不執行,直接整個方法返回,再拿下面的代碼做講解,如果lock方法直接返回,那麼說明鎖獲取成功,然後接着調用doSomeThing()方法,這就是爲什麼獲取鎖後,可以做後面的事情
Lock lock = new ReentrantLock();
try{
lock.lock();
doSomeThing();
}finally{
lock.unlock();
}
- 如果tryAcquire(arg)返回false呢,那就會執行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法了。這個方法就比較複雜了。先提前劇透下,讓讀者有個概念,後面就不那麼稀裏糊塗了。首先這個方法其實是分爲兩個部分。第一、調用addWaiter(Node.EXCLUSIVE),這個方法是什麼意思呢,還記得我們前面講過的AQS裏面會有隊列吧,這個方法做的事情也比較複雜,簡單來講就是,把獲取鎖失敗的線程封裝成Node,並加入到隊列中,but,隊列可能初始的時候是空的,所以還會有個初始化的操作哦。第二、acquireQueued(),簡而言之就是,線程在隊列中嘗試獲取鎖,但是要注意,在隊列裏面的線程可都是規規矩矩的,必須是排在隊首的纔有優先獲取鎖的權利,這個後面源碼解析部分會講到
3.曾經天真爛漫的我以爲acquireQueued()可能會很快執行完,那最終還是要調用selfInterrupt()的呀,那還不是會中斷線程,在lock()方法裏中斷線程,不是我想要的呀,doSomeThing()我都還沒機會執行呢。如果你也有同樣的疑惑,那麼我在劇透下,在acquireQueued()如果在隊列中獲取鎖失敗,當前線程是會被LockSupport.park()掉的,也就是阻塞,也就是說selfInterrupt()是沒有執行機會,如果線程在acquireQueued()在隊列中獲取到了鎖,線程還會判斷是否被中斷了,如果是被中斷纔會調用selfInterrupt()
我們先來總結下lock.lock()的流程,然後再詳細講解addWaiter()入隊和acquireQueued()線程Node在AQS隊列中嘗試獲取鎖的詳細實現
5. addWaiter入隊列
通過前面的分析我們知道,當線程獲取鎖失敗後,AQS會把線程封裝成Node對象,加到AQS的隊列中,詳細代碼如下
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
//如果隊列已經初始化了,直接把Node添加到隊列尾端
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果隊列沒有初始化調用enq
//enq做了兩件事 初始化隊列並把Node入隊
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {//注意死循環
Node t = tail;
if (t == null) { //如果沒有初始化嘗試初始化
if (compareAndSetHead(new Node()))
tail = head; //初始化成功接着循環,入隊
//走到這裏說明初始化失敗接着死循環初始化
} else {
//初始化成功 入隊 並終止循環
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
==注意!!!== ==AQS隊列 Head指向的節點是一個空節點node,它不代表任何線程,它只是爲了輔助找到隊列中等待最久的Node節點,node的下一個節點纔是真正意義上的隊首節點==
6. 線程節點在AQS隊列中獲取鎖
前面我們講了線程加入到AQS等待隊列中。當線程被加入到AQS隊列時,它會判斷線程所在的節點是否是隊首節點(==注意不是head所指向的節點,而是head指向的節點的下一個節點==),如果是隊首節點,會調用tryAcquire()獲取鎖,如果獲取成功結束上鎖。如果獲取失敗,會調用LockSupport.park()讓該線程阻塞
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {//又是死循環 只有獲取到鎖纔會終止
//獲取當前線程所代表的node的前面一個節點
final Node p = node.predecessor();
//如果前面節點是頭節點,嘗試獲取鎖
if (p == head && tryAcquire(arg)) {
//如果當前線程獲取鎖成功,將當前節點設爲頭節點(再次強調下,頭節點只是輔助的,真正的等待最久的節點是頭節點的下一個節點),並且直接return掉
setHead(node);
p.next = null; // help GC
return interrupted;
}
//獲取鎖失敗,判斷是否需要阻塞線程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
7.unlock()釋放鎖
我們都知道,當一個線程調用lock.unlock(),那麼該線程會釋放掉對鎖的使用權,其他新來的線程可以通過lock.lock()獲取鎖。那麼那些曾經想索取lock的使用權失敗的線程,他們被存儲在lock的sync對象的隊列中了,並且在多次嘗試加鎖失敗後,線程被強制阻塞了,他們是在什麼時候被喚醒並且再次得到競爭鎖的機會呢,下面通過探究unlock()方法來一探究竟。
unlock()相對lock()簡單一些。它的調用邏輯如下
Lock.unlock() –> sync.release(1) –> sync.tryRelease(1)
我們重點來講一下release 和tryRelease
public final boolean release(int arg) {
//嘗試釋放鎖
if (tryRelease(arg)) {
//釋放成功,獲取到AQS隊列的頭節點
Node h = head;
if (h != null && h.waitStatus != 0)
//從頭節點往隊尾找到一個沒有被取消的線程節點,喚醒它(只找一個不是所有的後面的節點)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//只有獲取到了鎖的線程纔有資格調用該方法,完美的解釋了沒有獲取鎖的線程直接調用unlock()會報IllegalMonitorStateException
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
8. 小結&一些嘮叨的話
到這裏已經基本上把Lock(主要是ReentrantLock)的lock()和unLock()方法做了一個比較全面的講解。如果有講錯或者遺漏的地方歡迎大家評論與我互動。接下來會講解下Condition在java同步包中的實現,todo list如下
[ ] 講解Condition的原理,Condition的隊列,
[ ] condition await()釋放鎖,park線程
[ ] condition signal(),把當前ConditionObject中的Node隊列enque到AQS隊列中去
最後歡迎添加我的微信,一起互動交流Java,一起成長