測試代碼:
https://github.com/kevindai007/springboot_houseSearch/tree/master/src/test/java/com/kevindai/juc
juc中的類太多,大分部又都需要些一個demo才能更好的理解,因此再開一篇
咱們首先開始研究LockSupport這個類,這個類是用來創建鎖和其他同步工具類的基本線程阻塞原語.Java鎖和同步器框架的核心AQS:AbstractQueuedSynchronizer,就是通過調用LockSupport.park()和LockSupport.unpark()實現線程的阻塞和喚醒的.LockSupport通過底層unsafe提供park,unpark操作.簡單點說:底層維護一個二義性的變量來保存一個許可,需要注意的是這個許可是一次性的,unpark操作設置該值爲1,park操作檢查該值是否爲1,爲1直接返回,不爲1,,則阻塞。
這個類的代碼都是很簡單的調用unsafe類的方法,沒什麼好分析的,咱們主要看下他的使用
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
//主線程一直處於阻塞狀態。因爲許可默認是被佔用的,調用park()時獲取不到許可,所以進入阻塞狀態
// LockSupport.park();
// System.out.println("block.");
//多次unpark,只有一次park也不會出現什麼問題,結果是許可處於可用狀態
// Thread thread = Thread.currentThread();
// LockSupport.unpark(thread);//釋放許可
// LockSupport.unpark(thread);//釋放許可
// LockSupport.park();// 獲取許可
// System.out.println("b");
// LockSupport是不可重入的,如果一個線程連續2次調用LockSupport.park(),那麼該線程一定會一直阻塞下去
// LockSupport.unpark(thread);
//
// System.out.println("a");
// LockSupport.park();
// System.out.println("b");
// LockSupport.park();
// System.out.println("c");
//線程如果因爲調用park而阻塞的話,能夠響應中斷請求(中斷狀態被設置成true),但是不會拋出InterruptedException
Thread t = new Thread(new Runnable()
{
private int count = 0;
@Override
public void run()
{
long start = System.currentTimeMillis();
long end = 0;
while ((end - start) <= 1000)
{
count++;
end = System.currentTimeMillis();
}
System.out.println("after 1 second.count=" + count);
//等待或許許可
LockSupport.park();
System.out.println("thread over." + Thread.currentThread().isInterrupted());
}
});
t.start();
Thread.sleep(2000);
// 中斷線程
t.interrupt();
System.out.println("main over");
}
}
非常重要
下面來研究一下abstractQueuedSynchronizer(簡稱aqs),aqs是一個線程同步的框架,也是整個juc包的基礎(Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock等類均在其基礎上完成的),下面咱們來看看其實現(爲方便理解,部分邏輯需要引用ReentrantLock的代碼來解釋)
首先很容易發現aqs有三個很重要的屬性:
//頭結點
private transient volatile Node head;
//尾節點
private transient volatile Node tail;
/**
* The synchronization state.
* 同步狀態
* state可以理解有多少線程獲取了資源,即有多少線程獲取了鎖,初始時state=0表示沒有線程獲取鎖.
*獨佔鎖時,這個值通常爲1或者0
*如果獨佔鎖可重入時,即一個線程可以多次獲取這個鎖時,每獲取一次,state就加1
*一旦有線程想要獲得鎖,就可以通過對state進行CAS增量操作,即原子性的增加state的值
*其他線程發現state不爲0,這時線程已經不能獲得鎖(獨佔鎖),就會進入AQS的隊列中等待.
*釋放鎖是仍然是通過CAS來減小state的值,如果減小到0就表示鎖完全釋放(獨佔鎖)
*/
private volatile int state;
下面來說一下aqs的大致邏輯
- AQS維護了一個隊列,並記錄隊列的頭節點和尾節點
- 隊列中的節點是獲取不到資源而阻塞的線程
- AQS同樣維護了一個狀態,這個狀態應該是判斷線程能否獲取到鎖的依據,如果不能,就加入到隊列
- 當某個節點獲取到資源後就移除隊列,然後讓其後面的節點嘗試獲取資源
下面咱們來看看Node節點是如何實現的
volatile Node prev;//此節點的前一個節點。
volatile Node next;//此節點的後一個節點
volatile Thread thread;//節點綁定的線程。
volatile int waitStatus;//節點的等待狀態
//節點狀態:取消狀態,該狀態表示節點超時或被中斷就會被踢出隊列
static final int CANCELLED = 1;
//節點狀態:等待觸發狀態,只有前一個節點的狀態爲SIGNAL時,當前節點的線程才能被掛起
static final int SIGNAL = -1;
//節點狀態:等待條件狀態,表明節點對應的線程因爲不滿足一個條件(Condition)而被阻塞。
static final int CONDITION = -2;
//節點狀態:狀態需要向後傳播,使用在共享模式頭結點有可能處於這種狀態,表示鎖的下一次獲取可以無條件傳播
static final int PROPAGATE = -3;
//需要補充的而是0時新節點纔會有的狀態
可以看出Node維護了一個雙向隊列,,並且每個節點都有自己的狀態
再看看AQS中定義的幾個重要的方法:
public final void acquire(int arg);//請求獲取獨佔式資源(鎖)
public final boolean release(int arg);//請求釋放獨佔式資源(鎖)
public final void acquireShared(int arg);//請求獲取共享式資源
public final boolean releaseShared(int arg);//請求釋放共享式資源
//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected int tryReleaseShared(int arg) {
throw new UnsupportOperationException();
}
可以看到aqs用acquire()和release()方法提供對資源的獲取和釋放
但是try**()結構的方法都是隻拋出了異常,很顯然這類方法是需要子類去實現的.
這也因爲AQS定義了兩種資源共享方式:Exclusive(獨佔,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可以同時執行,如Semaphone、CountDownLatch), AQS負責獲取資源(修改state的狀態),而自定義同步器負責就要實現上述方法告訴AQS獲取資源的規則.
下面來分析分析這幾個方法:
1、acquire(int)
此方法是aqs實現獨佔式資源獲取的頂層方法,這個方法和ReentrantLock.lock()等有着相同的語義.下面我們開始看源碼
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
這個函數共調用了4個方法, 其中tryAcquire(arg)是在子類Sync中實現, 其餘在均是AQS中提供.
而這個方法的流程比較簡單:
- tryAcquire()嘗試獲取資源,如果成功, 則方法結束
- addWaiter()方法以獨佔方式將線程加入隊列的尾部
- acquireQueued()方法是線程在等待隊列中等待獲取資源
- selfInterrupt(), 如果線程在等待過程中被中斷過,在這裏相應中斷.(線程在等待過程中是不響應中斷的,只有獲取資源後才能自我中斷)
下面來一一解讀這些方法:
(1)、tryAcquire()
此方法嘗試去獲取獨佔資源.如果獲取成功,則返回true,否則返回false。tryAcquire()方法前面已經說過,這個方法是在子類中是實現的. 而在ReentrantLock中,這個方法也正是tryLock()的語義.如下是ReentrantLock對tryAcquire()實現的源碼(ReentranLock中tryAcquire()與nonfairTryAcquire()一致):
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//等於0表示當前鎖未被其他線程獲取到
if (!hasQueuedPredecessors() //檢查隊列中是否有線程在當前線程的前面
&& compareAndSetState(0, acquires)) {//CAS操作state,鎖獲取成功
setExclusiveOwnerThread(current); //設置當前線程爲佔有鎖的線程
return true;
}
} else if (current == getExclusiveOwnerThread()) {//非0,鎖已經被獲取,並且是當前線程獲取.支持可重入鎖
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); //更改狀態位,
return true;
}
return false;//未能獲取鎖
}
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/**
*如果h=t,則隊列未被初始化,返回false
*如果隊列中沒有線程正在等待, 返回true
*如果當前線程是隊列中的第一個元素, 返回true,否則返回false
**/
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
(2)、sqc中addWaiter(int)
再看acquire()的第二個流程,獲取鎖失敗, 則將線程加入隊列尾部, 返回新加入的節點
private Node addWaiter(Node mode) {
//以獨佔模式構建節點,節點有共享和獨佔兩種模式
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//如果pred不爲空,說明有線程在等待
//嘗試使用CAS入列,如果入列失敗,則調用enq採用自旋的方式入列
//該邏輯在無競爭的情況下才會成功,快速入列
if (pred != null) {
node.prev = pred; //雙向隊列
if (compareAndSetTail(pred, node)) {//CAS更新尾部節點
//將原tail節點的後節點設置爲新tail節點
//由於CAS和設置next不是原子操作,因此可能出現更新tail節點成功,但是未執行pred.next = node,導致無法從head遍歷節點;
//但是由於前面已經設置了prev屬性,因此可以從尾部遍歷;
//像getSharedQueuedThreads、getExclusiveQueuedThreads都是從尾部開始遍歷
pred.next = node; //雙向隊列
return node;
}
}
enq(node); //如果隊列沒有初始化活更新尾部節點失敗,程序就會到這一步,通過自旋入列
return node;
}
private Node enq(final Node node) {
for (;;) {//自旋+CAS配合使用方式,一直循環知道CAS更新成功.
Node t = tail;
if (t == null) {//隊列爲空, 沒有初始化,必須初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { //設置尾節點,此時的head是頭節點,不存放數據
t.next = node;
return t;
}
}
}
}
(3)、sqc中acquireQueued()
addWaiter()完成後返回新加入隊列的節點, 緊接着進入下一個流程acquireQueued(), 在這個方法中, 會實現線程節點的阻塞和喚醒. 所有節點在這個方法的處理下,等待資源
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true; //是否拿到資源
try {
boolean interrupted = false; //等待過程中是否被中斷過
for (;;) { //又是一個自旋配合CAS設置變量
final Node p = node.predecessor(); //當前節點的前驅節點
if (p == head && tryAcquire(arg)) {//如果前驅節點是頭節點, 則當前節點已經具有資格嘗試獲取資源
setHead(node); //獲取資源後,設置當前節點爲頭節點
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果不能獲取資源,就進入waiting狀態
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //獲取前一個節點的狀態
if (ws == Node.SIGNAL)
/*
*此時前驅節點完成任務後能夠喚醒當前節
*記住,喚醒當前節點的任務是前驅節點完成
*/
return true;
if (ws > 0) { //ws大於0表示節點已經被取消,應該移出隊列.
do {
//節點的前驅引用指向更前面的沒有被取消的節點.所以被取消的節點沒有引用之後會被GC
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//找到了合適的前驅節點後,將其狀態設置爲SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
接下來是 parkAndCheckInterrupt() 方法, 真正讓節點進入waiting狀態的方法,是在這個方法中調用的.
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //使線程進入waiting狀態,查看上面的LockSupport類介紹
return Thread.interrupted(); //檢查是否被中斷
}
(4)、selfInterrupt()
acquire()方法不是立即響應中斷的. 由於線程獲取同步狀態失敗加入到同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移除,而是在獲取資源後進行自我中斷處理
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
至此獨佔鎖獲取資源的過程已經分析完了,理解流程只有也並不十分複雜,簡單來說就是嘗試獲取資源, 如果獲取不到就進入等待隊列變成等待狀態
2、release(int)
講了如何獲取到資源,接下來就應該如何釋放資源.這個方法會在獨佔的模式下釋放指定的資源(減小state),此方法與ReentrantLock.unlock()有相同的語意
public final boolean release(int arg) {
if (tryRelease(arg)) { //嘗試釋放資源
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); //喚醒隊列的下一個節點
return true;
}
return false;
}
分析釋放資源流程
(1)、tryRelease()這個方法是在子類中實現的.我們以ReentrantLock.unlock()爲例解讀資源釋放的過程
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //state減去指定的量,
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { //獨佔鎖模式下,state爲0時表示沒有線程獲取鎖,這時纔算是當前線程完全釋放鎖
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
(2)、unparkSuccessor()
此方法用於喚醒後繼節點
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) { //waitStatus表示節點已經被取消,應該踢出隊列
s = null;
//從後想前找到最靠前的合法節點
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
至此獨佔鎖的獲取、釋放資源流程都已經完了,我也是查的不少資料才把這個流程捋清楚,快給我點贊
上面分析了獨佔鎖的流程,下面咱們接着類分析共享鎖的過程
1、acquireShared()
此方法是aqs實現共享式資源獲取的頂層方法.下面我們開始看源碼
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
這個函數共調用了2個方法, 其中tryAcquireShared(arg)是在子類Sync中實現, doAcquireShared則是AQS中提供.
方法流程很簡單,首先嚐試獲取資源,如果狀態小於0(未獲取成功),則調用doAcquireShared()方法加入阻塞隊列.下面咱們分別來看看這兩個方法
(1)、tryAcquireShared()
tryAcquireShared()在aqs中僅是一個抽象方法,具體實現在子類中,這裏我以CountdownLatch爲例進行分析
//等於0表示當前鎖未被其他線程獲取到,即當前線程獲取到鎖時返回1,否則返回-1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
2、doAcquireShared()
共享模式獲取的核心公共方法,咱們看看源碼
private void doAcquireShared(int arg) {
//添加當前線程爲一個共享模式的節點,addWaiter()方法在獨佔模式是分析過,在此不做重複分析
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {//如果前驅節點是頭節點, 則當前節點已經具有資格嘗試獲取資源
int r = tryAcquireShared(arg);
//此時當state值大於0則認爲獲取成功
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//如果前驅節點不是頭節點則不能能獲取資源,就進入waiting狀態.判斷當前節點是否應該被阻塞,是則阻塞等待其他線程release
//此處的方法前面也分析過,在此不做研究
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果出異常,沒有完成當前節點的出隊,則取消當前節點
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);//把當前節點設爲頭結點
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())//如果後繼節點爲共享模式且參數propagate是否大於0或者PROPAGATE是否已被設置,則喚醒後繼節點
doReleaseShared();
}
}
這樣共享鎖的基本流程就結束了,簡單來說就是嘗試獲取資源,如果獲取不到就加入隊列中等待.與獨佔鎖不同的是,獨佔鎖嘗試獲取資源時會檢查隊列中是否有其他線程,如果沒有就設置當前線程爲佔有鎖的線程,即只有一個線程持有資源;而共享模式當調用doAcquireShared時,會看後續的節點是否是共享模式,如果是,會通過doReleaseShared()喚醒後續節點,讓所有等待的共享節點獲取資源
下面來分析一下釋放資源的過程
1、releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//嘗試釋放資源
doReleaseShared();
return true;
}
return false;
}
咱們直接去CountdownLatch中看看tryReleaseShared()方法
protected boolean tryReleaseShared(int releases) {
for (;;) {//自旋+cas改變狀態
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
再來看看doReleaseShared()方法,這是共享模式釋放資源的核心方法
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {/如果等待隊列中有等待線程
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//此方法前面分析過,用於喚醒後續節點
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
此方法邏輯也比較簡單,就是喚醒第一個等待節點.但需要注意的是,根據前面acquireShared的邏輯,被喚醒的線程會通過setHeadAndPropagate繼續喚醒後續等待的線程
到這裏AQS就分析完了,到這裏應該對獨佔鎖、共享鎖有一個認識,不清楚沒關係,後續咱們會在其實現類中結合實際情況,進行更加深入的分析,如果有什麼想討論的歡迎留言一起討論