目錄
下面的源碼分析將圍繞三個線程的使用來介紹源碼,先圖解,後源碼解釋,這裏只講公平鎖!!!
藉助代碼分析源碼
import java.util.concurrent.locks.ReentrantLock;
/**
* 三個線程進來,阻塞兩個,進行ReentrantLock源碼分析
* @author xuexue
*
*/
public class Test {
//創建ReentrantLock對象
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
//創建線程t1
Thread t1 = new Thread("t1"){
public void run() {
//給t1加鎖
reentrantLock.lock();
try {
//打印線程名
System.out.println(getName());
//讓線程t1睡眠,讓線程t2、t3進入阻塞,進入隊列
sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {//釋放t1鎖
reentrantLock.unlock();
}
}
};
//創建線程t2
Thread t2 = new Thread("t2"){
public void run() {
//給t2加鎖
reentrantLock.lock();
try {
//打印線程名
System.out.println(getName());
} finally {//釋放t2鎖
reentrantLock.unlock();
}
}
};
//創建線程t3
Thread t3 = new Thread("t3"){
public void run() {
//給t3加鎖
//reentrantLock.lock();
try {
//打印線程名
System.out.println(getName());
} finally {//釋放t3鎖
//reentrantLock.unlock();
}
}
};
//分別啓動三個線程
t1.start();
t2.start();
t3.start();
}
}
圖片分析過程
1、當t1進來拿到鎖之後,緊接着t2上來拿鎖,進行初始化隊列
2、隊列初始化成功,線程t2入隊
3、t3入隊
4、t1釋放鎖後,t2拿到鎖出隊
5、t2釋放鎖後,t3拿到鎖出隊
源碼分析
一、t1線程拿鎖源碼分析過程
1、進入lock()方法
2、繼續sync進入lock()方法-----(sync是Sync類,繼承了AQS的內部類)
3、進入acquire(1)方法-----參數1是cas嘗試拿鎖操作用的
4、進入tryAcquire方法-----t1是第一個線程,拿鎖成功,方法結束,返回ture
5、全部返回,代碼繼續向下執行,執行代碼輸出t1
但是遇到sleep(2000)睡眠2s,這裏故意設置睡眠,從而達到讓t1/t2/t3線程存在競爭,讓t2/t3進入隊列並阻塞
二、t2線程拿鎖失敗進入隊列阻塞源碼分析過程
7、因爲t1休眠,此時cpu分配給了t2(或者t3這裏假設分配給了t2)
8、繼續1、2、3步操作,繼續分析
此時state=1,拿鎖失敗,並且t2不是鎖持有線程t1,所以返回false
9、執行入隊操作,進入方法addWaiter() 參數Node.EXCLUSIVE=null是一個Node節點
10、進入addWaiter 方法-----創建線程t2的節點,獲取隊尾節點,隊尾節點爲null,所以進入end()方法
11、進入end()方法-----初始化隊列過程
第一次循環,tail=null,new Node() 創建了一個新的節點cas操作,將head設置爲這個新節點,並將tail也設置爲這個節點
第二次循環,將t2線程的Node節點前驅指向head,進行cas操作,將t2線程的Node設置爲tail 就是tail=node,將head.next=node 鏈表關係建立好,隊列就是維護鏈表的關係
aqs爲什麼不一開始初始化隊列?因爲如果是單線程或者線程交替執行的時候就不需要用到隊列,當有競爭的時候才需要
此過程對應的圖解(但是ws還沒設值,在13步設值)
12、將t2線程的Node節點返回,進入acquireQueued()方法-----
拿到t2 node的前驅head,進入判斷,進入自旋,t1未釋放鎖,自旋拿鎖失敗
第一次自旋拿鎖失敗進入shouldParkAfterFailedAcquire(參數1:head,參數2:t2線程的Node節點)
(下次還會自旋一次,這裏設置自旋,爲了儘量不讓線程park進入阻塞,儘量在java級別實現,不調用os的api,提高效率)
13、進入shouldParkAfterFailedAcquire(p, node)方法----
perd.waitStatus就是head.waitStatus,初始化值爲0,2線程 直接進入 cas操作 將head的waitStatus置爲-1
返回false,結束方法,進入第二次循環,第二次自旋
(每次入隊都會將上一個Node的waitStatus置爲-1狀態,目的也是爲了多自旋一次)
爲什麼阻塞狀態ws=-1要讓下一個隊列線程來修改?(入隊的時候會把上一個線程狀態ws改爲-1)
1、因爲線程阻塞park了,自己不能修改ws狀態,不能之前修改,怕出異常了
2、睡眠了是讓隊列下一個線程看到的,自己看不看無關緊要
14、進行第二次自旋,目的還是爲了儘量不讓線程park進入阻塞,儘量在java級別實現,不調用os的api,提高效率
15、第二次進入shouldParkAfterFailedAcquire(p, node)-----在上一次head.waitStatus已經被設置爲-1,直接返回true
16、進入parkAndCheckInterrupt()-----阻塞park隊列中的t2線程,在這個位置阻塞,下次喚醒也是這個位置,記住了
方法不能正常返回結束,代碼被阻塞在這裏,無法繼續往下執行,等待喚醒
三、t3線程拿鎖失敗進入隊列阻塞源碼分析過程
17、t2被阻塞後,因爲t1休眠(休眠時間夠長),此時cpu分配給了t3,線程t3此時進來
18、繼續1、2、3步驟往下執行
此時state=1,拿鎖失敗,並且t3不是鎖持有線程t1,所以返回false
19、進入addWaiter 方法-----創建線程t3的節點,獲取隊尾節點,隊尾節點爲t2線程的Node
線程t3入隊操作,維護隊列關係
此過程對應的圖解(但是ws還沒設置)
20、將t2線程的Node節點返回,進入acquireQueued()方法-----
拿到t3線程Node的前驅t2線程的Node,不會自旋拿鎖,直接進入shouldParkAfterFailedAcquire(參數1:t2線程的Node,參數2:t3線程的Node節點)
(下次還會自旋一次,這裏設置自旋,爲了儘量不讓線程park進入阻塞,儘量在java級別實現,不調用os的api,提高效率)
21、進入shouldParkAfterFailedAcquire()方法------
pred.waitStatus就是t2線程Node的狀態,就是0,第一次進入時,將隊列上一個元素的waitStatus置爲-1,返回false
22、第二次進入shouldParkAfterFailedAcquire()-----在上一次t2.waitStatus已經被設置爲-1,直接返回true
23、進入parkAndCheckInterrupt()-----阻塞park隊列中的t2線程,在這個位置阻塞,下次喚醒也是這個位置,記住了
方法不能正常返回結束,代碼被阻塞在這裏,無法繼續往下執行,等待喚醒
四、t1釋放鎖源碼分析過程
24、線程t2、t3都已經被阻塞,等待t1釋放鎖被喚醒,此時t1釋放鎖
25、進入unlock()方法-----
26、進入release()方法-----嘗試釋放鎖
27、進入tryRelease(arg)方法-----
t1線程只上了一次lock(),所以state爲1,c=state-1=0,就是釋放鎖,並將持有鎖線程置爲空,將state設置爲0,返回true
(如果多次加鎖,需要多次釋放鎖纔可以完全釋放)
28、返回true,進入if 語句體,拿到head節點,調用unparkSuccessor() 喚醒頭節點Node的下一個元素
29、進入unparkSuccessor()-----
將head的waitStatus置爲0,喚醒head下一個Node的線程,這裏就是t2線程,方法結束
30、一直返回,直到finnally結束,進入喚醒線程位置,就是當時被阻塞的位置
五、t2線程被喚醒後拿鎖源碼分析過程
31、t2線程被喚醒,進入t2當時被阻塞的位置,繼續執行代碼,代碼返回false
32、繼續for循環一次,拿鎖成功,返回false,方法層層返回
對應的圖解過程
33、方法正常返回,輸出t2線程名字,繼續執行,釋放t2線程鎖
六、t2線程釋放鎖源碼分析過程
34、進入unlock()方法,release()方法-----
35、進入unparkSuccessor()方法-----喚醒t3線程,結束方法,層層返回
36、結束t2線程,進入t3被阻塞的位置繼續執行
七、t3線程被喚醒後拿到鎖源碼分析過程
37、t3線程被喚醒,進入t3當時被阻塞的位置,繼續執行代碼,代碼返回false
38、繼續for循環一次,拿鎖成功,返回false,方法層層返回
對應的圖解過程
39、方法正常返回,輸出t3線程名字,程序繼續執行,釋放t3鎖
八、t3線程釋放鎖源碼分析過程
40、進入unlock()方法,release()方法----- 因爲此時隊列沒有線程的Node,head的waitStatus=0,返回false,結束方法,層層返回
此時的head和tail的狀態
41、t3釋放鎖成功,三個線程執行完畢,程序結束
42、輸出結果(t1線程立即輸出,t2、t3隔兩秒輸出。也有可能t3在t2前面執行,上面源碼分析假設t2在t3前面執行)