前言 AQS一共花了5篇文章,對裡面實現的核心源碼都做了註解 也和大家詳細描述了下,後面的幾篇文字我將和大家聊聊一下AQS的實際使用,主要會聊幾張鎖,第一篇我會和大家聊下ReentrantLock 重入鎖,雖然在講AQS中也穿插了講了一下,但是我還是深入的聊下 ==PS:前面5篇寫在了CSDN裡面 ...
前言
AQS一共花了5篇文章,對裡面實現的核心源碼都做了註解 也和大家詳細描述了下,後面的幾篇文字我將和大家聊聊一下AQS的實際使用,主要會聊幾張鎖,第一篇我會和大家聊下ReentrantLock 重入鎖,雖然在講AQS中也穿插了講了一下,但是我還是深入的聊下
PS:前面5篇寫在了CSDN裡面 懶得轉過來來了,有興趣的移步去看下吧
- Java-AQS同步器 源碼解讀1-獨占鎖加鎖
- Java-AQS同步器 源碼解讀2-獨占鎖解鎖
- Java-AQS同步器 源碼解讀3-共用鎖
- Java-AQS同步器 源碼解讀4-條件隊列上
- Java-AQS同步器 源碼解讀5-條件隊列下
ReentrantLock簡介
ReentrantLock中文翻譯:重入鎖。那具體重入是什麼意思呢,如果看過前面幾篇文章的人,應該瞭解一下,簡答的說就是AQS同步器裡面的State相當於一個計數器,如果某一個線程獲取鎖了以後再再次去獲取鎖,這個計算器State就會+1.後面的代碼中會詳細的描述到。
還有一個重要的點 就是lock和Condition的聯合使用,ReentrantLock可以創建一個Condition,這個我在條件隊列的文章中詳細描述過。
Synchronized對比
ReentrantLock是一個排他鎖,也就是同一個時刻只會有一個線程能獲取到鎖,這個主要利用的就是AQS的獨占模式實現的。ReentrantLock能保證在多線程的情況下的線程執行安全,那就會想到Synchronized的關鍵字。Synchronized是JVM實現的,ReentrantLock是由JDK實現的,ReentrantLock相對於比較靈活,可以設置時間等待,線程中斷,鎖投票等,但是一定用完記得要在finally手動釋放,Synchronized是JVM做自動釋放鎖的操作。
用法
/**
* @ClassName ReentrantLockDemo
* @Auther burgxun
* @Description: 重入鎖的Demo
* @Date 2020/4/5 14:21
**/
public class ReentrantLockDemo {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
reentrantLock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("獲取鎖的線程是:" + finalI);
reentrantLock.unlock();
}).start();
}
}
}
執行結果是:
org.example.ReentrantLockDemo
獲取鎖的線程是:1-開始執行
獲取鎖的線程是:1-執行結束
獲取鎖的線程是:2-開始執行
獲取鎖的線程是:2-執行結束
獲取鎖的線程是:4-開始執行
獲取鎖的線程是:4-執行結束
獲取鎖的線程是:3-開始執行
獲取鎖的線程是:3-執行結束
獲取鎖的線程是:5-開始執行
獲取鎖的線程是:5-執行結束
Process finished with exit code 0
從執行結果上 我們可以看到 一個線程執行完成釋放鎖後才能執行另外一個線程
源碼分析
看完了上面的簡介和用法,我們進入源碼去分析看下 是怎麼實現的
代碼結構
方法分析
從UML類圖上面我們可以看到 ReentrantLock有3個內部類,一個是抽象的靜態類Sync還有2個實現了Sync的類一個是非公平鎖的實現NonfairSync,還有一個是公平鎖的實現FairSync
Sync
首先我們看下Sync這個抽象類
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
/**
* 非公平鎖鎖的tryAcquire的實現 AQS中tryAcquire方法的需要子類重寫
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//獲取當前線程
int c = getState();//獲取CAS的State值
if (c == 0) {//如果等於0 說明可以獲取鎖
if (compareAndSetState(0, acquires)) {//CAS的方式 修改State 狀態
setExclusiveOwnerThread(current);//CAS修改成功後 設置當前線程為鎖的持有者
return true;
}
} else if (current == getExclusiveOwnerThread()) {//判斷當前線程 是否是鎖的持有者線程
int nextc = c + acquires;//新增重入次數
if (nextc < 0) // overflow //如果小於0 說明是出問題了 拋出異常
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
/**
* AQS 中方法的重寫 釋放資源
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// 算下 當前同步器中state的差值
//確保釋放和加鎖線程是同一個 上一篇中我也提到過
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;//是否完全了釋放資源
if (c == 0) {//如果C的值是0 說明沒有線程擁有鎖了
free = true;
/*
* 設置擁有鎖的線程為null 因為現在鎖是沒線程暫用的 如果不修改 下次別的線程去獲取鎖的會有這個判斷
*/
setExclusiveOwnerThread(null);
}
setState(c);//修改 同步器的State 狀態
return free;
}
/**
* 判斷當前線程 是否是擁有鎖的線程一致 重寫了AQS的方法
*/
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
/**
* 獲取當前重入鎖的擁有者
*/
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
/**
* 當前鎖占用的State 數量 也就是重入了幾次
*/
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
/**
* 重入鎖 是否可以被占用 State等於0 別的線程才能來搶占到鎖
*/
final boolean isLocked() {
return getState() != 0;
}
}
上面是我寫了註解的Sync的整個類,一會兒我們挨個分析下,從繼承關係 我們可以看到Sync是繼承於我們的AQS的,所以裡面很多底層的方法都是用的AQS裡面的實現,所以說呀 理解了AQS 那整個JUC下的鎖和各種線程安全的集合什麼的 看源碼都會輕鬆很多~
NonfairSync
//非公平鎖
static final class NonfairSync extends Sync {
@Override
void lock() {
if (compareAndSetState(0, 1))//如果CAS能設置成功 說明能獲取到鎖 就設置當前線程為鎖的owner 不公平的體現
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
/**
* 嘗試獲取資源,覆寫的AQS類裡面方法
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
FairSync
//公平鎖
static final class FairSync extends Sync {
@Override
void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/*
* hasQueuedPredecessors 返回是否Sync中是否有在當前線程前面等待的線程 false沒有
* 如果false 那就CAS 修改State 成功後更新當前線程是鎖的擁有者線程
* */
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;
}
}
非公平鎖VS公平鎖
什麼是公平非公平
看了上面的代碼 有人可能不明白什麼叫做TMD公平!!!哈哈
具體的代碼下文描述,我先舉個小例子,讓讀者帶入下:
話說大家在火車站排隊買票,小明和小強放寒假了,都去火車站提前買票準備回家,他們倆到了車站一看,售票視窗就開了一個,而且前面有3個人在排隊,那小明和小強沒辦法就只能排除在了隊伍裡面,等了5分鐘終於排到了小強,小強剛買完票,突然看到同個宿舍的小張也來買票了,立馬和小張說,來來來 這邊 我這邊可以買票,小張立馬插隊到了小強後面,強行去買了票,小明心裡嘀咕說居然插隊,素質真差,看你人高馬大的 就算了吧!哈哈~
其實上面的類子中 公平和非公平的體現就是 賣票的視窗只有一個,就像獲取獨占鎖,當有一個線程占有了鎖,那其餘的線程就必須在後面排隊等待,就像買票一樣,非公平的鎖的實現就相當於插隊,管你後面有沒有人 我都要去嘗試下買票
從上面的分析我們也能知道公平和非公平是指的是獲取資源時候的行為。
ReentrantLock
ReentrantLock的構造函數
/**
* 預設實現非公平鎖
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 帶參數的構造函數
*/
public ReentrantLock(boolean fair) {
sync = fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync();
}
從上面可以看到ReentrantLock預設使用的是一個非公平鎖的實現
lock加鎖方法
/**
* 加鎖
*/
@Override
public void lock() {
sync.lock();
}
/**
* 加鎖 響應中斷
*/
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
/**
* 只做一次獲取鎖的嘗試 不會阻塞線程
*/
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
@Override
/**
* 只做一次獲取鎖的嘗試 不會阻塞線程
*/
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
非公平的加鎖
從代碼中 我們看到lock方法調用的是Sync的lock方法,但是Sync中的lock方法是一個抽象方式是子類實現的
那我從NonfairSync類中找到了lock的具體實現,入下:
void lock() {
//如果CAS能設置成功 說明能獲取到鎖 就設置當前線程為鎖的owner 不公平的體現
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先方法一開始先執行一個CAS修改State的操作,如果能夠執行成功,說明當前的AQS同步器中的狀態值是0,那麼線程就可以占有鎖,然後設置當前線程為鎖的Owner線程.setExclusiveOwnerThread這個方法我在第一篇文章中也描述過,這個方法是在AQS的父類AbstractOwnableSynchronizer方法裡面的!
如果執行失敗,那麼方法就執行acquire方法:
/**
* 此方法位於AQS中
* 獲取資源
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* 此方法位於NonfairSync中
* 嘗試獲取資源,覆寫的AQS類裡面方法
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* 此方法位於Sync中
* 非公平鎖鎖的tryAcquire的實現 AQS中tryAcquire方法的需要子類重寫
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//獲取當前線程
int c = getState();//獲取CAS的State值
if (c == 0) {//如果等於0 說明可以獲取鎖
if (compareAndSetState(0, acquires)) {//CAS的方式 修改State 狀態
setExclusiveOwnerThread(current);//CAS修改成功後 設置當前線程為鎖的持有者
return true;
}
} else if (current == getExclusiveOwnerThread()) {//判斷當前線程 是否是鎖的持有者線程
int nextc = c + acquires;//新增重入次數
if (nextc < 0) // overflow //如果小於0 說明是出問題了 拋出異常
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
acquire方法位於AQS中 此方法會首先調用tryAcquire方法去嘗試獲取下鎖,因為雖然lock方法一開始獲取鎖失敗了,可能這邊鎖又被別的線程釋放了,所以要再次嘗試獲取下鎖,具體tryAcquire怎麼做的 上面的代碼中我已經描述的很清楚了~
公平的加鎖
先看下代碼:
/**
* 公平鎖版本的加鎖
*/
void lock() {
acquire(1);
}
/**
* 此方法位於AQS中
* 獲取資源
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/*
* hasQueuedPredecessors 返回是否Sync中是否有在當前線程前面等待的線程 false沒有
* 如果false 那就CAS 修改State 成功後更新當前線程是鎖的擁有者線程
* */
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;
}
首先這邊lock的時候,直接執行了acquire方法,而非和NonfairSync一樣,有個CAS的嘗試操作,這也是體現公平的一部分,和非公鎖的方法一樣的是這邊的acquire方法也是在AQS上中的,但是調用的tryAcquire方法 是自己fairSync類自己實現的,而非調用的Sync的預設實現,這邊唯一有區別的就是hasQueuedPredecessors 這個方法,hasQueuedPredecessors方法是獲取當前Sync隊列中是否還有別的等待線程,如果有 就算當前的狀態State是滿足條件的,也是要加入Sync等待隊列中的,這個是acquireQueued方法裡面做的事情,不清楚這個acquireQueued方法的,看看前面幾篇文章,我就不再贅述了~
unlock解鎖
@Override
public void unlock() {
sync.release(1);//調用的AQS裡面的方法
}
/**
* 此方法在AQS中
* 釋放當前資源
* 喚醒等待線程去獲取資源
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {//說明釋放資源成功
Node h = head;//當前隊列裡面的head節點
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//通知阻塞隊列裡面的head的next節點去獲取鎖
return true;
}
return false;
}
/**
* 此方法在Sync中
* AQS 中方法的重寫 釋放資源
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// 算下 當前同步器中state的差值
//確保釋放和加鎖線程是同一個 上一篇中我也提到過
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;//是否完全了釋放資源
if (c == 0) {//如果C的值是0 說明沒有線程擁有鎖了
free = true;
/*
* 設置擁有鎖的線程為null 因為現在鎖是沒線程暫用的 如果不修改 下次別的線程去獲取鎖的會有這個判斷
*/
setExclusiveOwnerThread(null);
}
setState(c);//修改 同步器的State 狀態
return free;
}
從源碼上看 解鎖的方法只有一個 因為公平鎖和非公共鎖 只是描述的是加鎖的行為,解鎖的行為其實都是一致的,都是釋放當前線程占用State值,然後喚醒SyncQueue的頭部Head節點的下一個節點去嘗試獲取鎖!
有些沒帖子上面的方法描述 請在前面AQS中的幾篇文章中看下 都詳細描述過~
總結
公平鎖 VS 非公平鎖
首先2者並沒有好壞之分,是要根據對應的場景選擇對應的鎖技術
公平鎖 則重的是公平性
非公平鎖 則重的是併發性
非公平鎖 是搶占式的,忽略了SyncQueue重其他的等待線程,線程在進入等待隊列之前會進行2次嘗試獲取鎖,這大大增加了獲取鎖的機會,這種好處體現在2個方面:
- 線程不必加入等待隊列就可以獲取鎖,不僅免去了構造節點並加入隊列的繁瑣操作,同時也節省了線程阻塞的喚醒開銷,線程阻塞和喚醒涉及到線程上下文的切換和操作系統的系統調用,是非常耗時的。在高併發的情況下,如果線程持有鎖的時間非常短,短到線程入隊阻塞的過程超過了線程持有並釋放鎖的時間的開銷,那麼這種搶占式的特性對併發的性能的提高會很明顯
- 減少CAS競爭,如果線程必須要加入阻塞隊列才能去獲取鎖,那麼入隊時的CAS競爭將變得異常的激烈,CAS操作雖然不會導致線程失敗而掛起,但不斷的失敗重試導致對CPU的浪費是不能忽略的
Synchronized VS ReentrantLock
從整個文章的分析來看,ReentrantLock是比Synchronized更加的靈活的,
- ReentrantLock提供了更多 更全面的API 可以設置等待時間,可以中斷方法等,還提供了Trylock等非阻塞的方法
- ReentrantLock還可以配和Condition一起使用,使得線程等待的時候更加靈活,可以設置不同的條件等待 等等