前言
談到多線程跃惫,一般都會(huì)聯(lián)想到高并發(fā),但是實(shí)際上兩者并不是一個(gè)概念艾栋,高并發(fā)一般指的是從業(yè)務(wù)方面的描述系統(tǒng)的并發(fā)負(fù)載能力爆存,而多線程只不過是如何使CPU的利用率達(dá)到最大化。因此一般問到高并發(fā)蝗砾,都會(huì)從你的項(xiàng)目業(yè)務(wù)角度出發(fā)先较,偏向于實(shí)戰(zhàn)方面,而多線程一般是問底層的一些編程技術(shù)方面的問題悼粮。
當(dāng)然闲勺,如果沒有掌握多線程的技術(shù),那就不用談所謂的高并發(fā)場(chǎng)景了扣猫。因此我們接下來先了解 Java 當(dāng)中多線程的一些基本知識(shí)點(diǎn)以及相應(yīng)的面試題菜循,而 JVM 層面的多線程面試題即JMM已經(jīng)在《漫談面試系列》——JVM(2)中談及,如果讀者沒有了解這方面的內(nèi)容苞笨,可以先前往該章節(jié)了解一下為什么我們需要關(guān)注線程安全問題债朵。
下面我們通過幾道常問的面試題進(jìn)行相關(guān)知識(shí)點(diǎn)的學(xué)習(xí):
1. 實(shí)現(xiàn)多線程的方式有哪些,ThreadLocal使用中需要注意什么瀑凝?
2. CAS 是什么序芦?CAS 有什么缺陷,如何解決粤咪?
3. AQS 是什么谚中,它有什么作用?
1. 多線程的實(shí)現(xiàn)方式以及ThreadLocal注意點(diǎn)
這里主要介紹一下 Future/Callable 的多線程實(shí)現(xiàn)方式寥枝,其余兩種應(yīng)該就算是初學(xué)者都懂的了宪塔。首先 Callable 算是 Runnable 接口的一個(gè)補(bǔ)充,因?yàn)?Runnable 的 run() 方法是不帶返回值的囊拜,但是 Callable 的 call() 方法是帶返回值某筐,而為了不破壞原有的代碼和風(fēng)格,這個(gè) call() 方法會(huì)在 run() 方法里面調(diào)用冠跷,代碼如下:
//該run方法由FutureTask類實(shí)現(xiàn)
public void run() {
if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//調(diào)用call方法
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
Callable 接口提供了帶返回值的線程方法南誊,但是具體如何操作 Callable 接口則由 Future 接口的實(shí)現(xiàn)類進(jìn)行,并且其最終實(shí)現(xiàn)類有且唯有 FutureTask 類蜜托。
從上圖可以看出抄囚,F(xiàn)utureTask 實(shí)現(xiàn)了 Future 接口,那么它便獲得了針對(duì) Callable 實(shí)現(xiàn)類的一些操作橄务,例如獲取返回值(阻塞)幔托、判斷任務(wù)是否完成、判斷任務(wù)是否被取消蜂挪、取消任務(wù)等重挑;
同時(shí)它也實(shí)現(xiàn)了 Runnable 接口,那么它可以被 Thread 類使用棠涮。下面我們通過代碼看看 Future 接口有哪些關(guān)乎 Callable 操作的方法:
public interface Future<V> {
//取消任務(wù)
boolean cancel(boolean mayInterruptIfRunning);
//任務(wù)是否結(jié)束攒驰,無論是完成導(dǎo)致的結(jié)束還是異常導(dǎo)致的結(jié)束
boolean isCancelled();
//任務(wù)是否完成
boolean isDone();
//阻塞式獲取,只要 call() 方法沒有接受故爵,就一直阻塞
V get() throws InterruptedException, ExecutionException;
//阻塞式獲取玻粪,但是帶有時(shí)間參數(shù),超過時(shí)間則直接返回
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
總的來說诬垂,我們需要定義 Callable 的 call() 方法劲室,這個(gè)方法可以類比為平時(shí)我們 Runnable 的 run() 方法,然后將 Callable 對(duì)象傳入給 FutureTask 類结窘,在 FutureTask 的 run() 方法中調(diào)用這個(gè) call() 方法很洋,除此之外,如果我們想得知目前該線程的執(zhí)行狀態(tài)和返回結(jié)果隧枫,也可以通過 FutureTask 類的一些方法如 isCancelled()喉磁、isDone()谓苟、get()等方法。接下來我們通過一個(gè)圖以及簡單的代碼demo了解下如何使用 FutureTask 實(shí)現(xiàn)多線程协怒。
public void future(){
//這里通過lambda表達(dá)式直接生成匿名的Callable類
FutureTask futureTask = new FutureTask(() -> {
System.out.println("futureTask demo");
return "demo";
});
Thread thread = new Thread(futureTask);
thread.start();
//判斷是否在運(yùn)行
System.out.println(futureTask.isDone());
//判斷是否以及取消
System.out.println(futureTask.isCancelled());
try {
//獲取返回值
System.out.println(futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
這里不過多闡述 FutureTask 的內(nèi)部細(xì)節(jié)涝焙,只為了讓讀者可以理清這種線程方式的邏輯,當(dāng)然 FutureTask 的核心還是在于它提供的異步操作的方法孕暇,這些內(nèi)容讀者可以參考相關(guān)文章仑撞。
很多業(yè)務(wù),我們需要在多線程環(huán)境下進(jìn)行一些變量的傳遞妖滔,而如果我們使用共享變量隧哮,又會(huì)由于多線程環(huán)境下一致性的問題導(dǎo)致業(yè)務(wù)出錯(cuò),針對(duì)這種共享變量的一致性座舍,我們只能上鎖進(jìn)行操作沮翔,這種行為相對(duì)來說非常損耗資源;
而如果我們使用局部變量曲秉,又會(huì)在方法的調(diào)用上難以傳參鉴竭,如果我的方法棧很深的話,便會(huì)有大量的方法的形參都得加上這個(gè)局部變量岸浑。
因此搏存,java 提供了一個(gè)方便我們線程隔離的局部變量工具類 ThreadLocal ,假如我們業(yè)務(wù)上不需要共享變量只需要局部變量的話矢洲,可以通過該工具類方便的進(jìn)行方法的調(diào)用傳參璧眠,保證每個(gè)線程獲取到的數(shù)據(jù)都是屬于各自線程的。
ThreadLocal 本質(zhì)上并不存儲(chǔ)任何東西读虏,最終存儲(chǔ)的內(nèi)容都是存放到 Thread 類的一個(gè)內(nèi)部 Map 類當(dāng)中责静,既然說到 Map,那肯定有 key 也有 value盖桥,而 ThreadLocal 對(duì)象便是這個(gè) Map 的 key灾螃,我們只要看看 ThreadLocal 的一個(gè) get()/set() 方法源碼,相信讀者就理解這個(gè)類到底是做什么用了揩徊。
public T get() {
//獲取當(dāng)前線程
Thread t = Thread.currentThread();
//然后在這個(gè)線程里面獲取上面提到的 map
ThreadLocalMap map = getMap(t);
if (map != null) {
//通過this來獲取對(duì)應(yīng)的value腰鬼,這個(gè)this便是ThreadLocal對(duì)象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
//這個(gè)類的方法都會(huì)通過這個(gè)語句獲取當(dāng)前的線程
Thread t = Thread.currentThread();
//然后在這個(gè)線程里面獲取上面提到的 map
ThreadLocalMap map = getMap(t);
if (map != null)
//set操作是將 this 作為 key,至于這個(gè) key 便是 ThreadLocal 對(duì)象了
map.set(this, value);
else
createMap(t, value);
}
//獲取線程的map對(duì)象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
基本上這兩個(gè)方法已經(jīng)表明了 ThreadLocal 到底是如何實(shí)現(xiàn)線程隔離的塑荒,其實(shí)數(shù)據(jù)本身就是存到對(duì)應(yīng)的 Thread 對(duì)象當(dāng)中熄赡,肯定是線程隔離的,只不過我們通過這個(gè) ThreadLocal 作為 key 齿税,方便我們獲取其中的數(shù)據(jù)而已彼硫,同時(shí),如果你想一個(gè)線程擁有多個(gè)局部變量,那么你就得申明多個(gè) ThreadLocal 對(duì)象進(jìn)行使用拧篮。
介紹完ThreadLocal 的基本知識(shí)词渤,接下來我們回到問題,我們使用 ThreadLocal 的時(shí)候需要注意什么串绩。
- 首先缺虐,我們的數(shù)據(jù)最終都是存儲(chǔ)在線程 Thread 類自身,因此如果我們使用線程池的時(shí)候赏参,由于存在線程的重復(fù)利用志笼,那么就有可能出現(xiàn)這個(gè)線程里面的 map 存有了不同業(yè)務(wù)環(huán)境的變量沿盅,最終可能導(dǎo)致業(yè)務(wù)方面的數(shù)據(jù)問題把篓,因此如果我們使用了線程池,務(wù)必在使用了 get() 方法后并且保證后續(xù)不需要這個(gè)變量了腰涧,在 finally 塊當(dāng)中調(diào)用 remove() 方法韧掩。
- 還有一點(diǎn)便是,由于 Thread 類中的 ThreadLocalMap 并不知道對(duì)應(yīng)的 key 什么時(shí)候會(huì)沒有引用指向窖铡,因此它無法判斷這個(gè) key 到底還有沒有用疗锐,于是它通過一個(gè)弱引用的形式來存儲(chǔ)這個(gè) ThreadLocal 對(duì)象作為 key ,只要外界沒有引用指向這個(gè) ThreadLocal 對(duì)象费彼,那么GC的時(shí)候便會(huì)自動(dòng)清理掉這個(gè) ThreadLocal 對(duì)象滑臊,不過這個(gè)帶來的問題便是,會(huì)導(dǎo)致 map 里面出現(xiàn)一對(duì) <null,Value> 的情況箍铲,假如出現(xiàn)這種情況后雇卷,線程一直沒有銷毀,也沒執(zhí)行 get\set\remove 方法的話颠猴,那么這個(gè) Value 便會(huì)一直存在关划,最終導(dǎo)致內(nèi)存泄漏,因此每次使用完 ThreadLocal 對(duì)象翘瓮,盡量在調(diào)用一次 get\set\remove 方法贮折,因?yàn)檫@些方法會(huì)將 map 中 key 為 null 的鍵值對(duì)刪除。
2. CAS是什么资盅?CAS 有什么缺陷调榄,如何解決?
CAS 全稱是 Compare And Swap 呵扛,即比較與交換振峻,它由一條指令完成,是原子性的择份,如果多個(gè)線程對(duì)同一個(gè)共享變量執(zhí)行 CAS 操作扣孟,最終只會(huì)有一個(gè)線程可以操作成功,操作成功返回 true荣赶,反之返回 false凤价。
在 Java 的 sun.misc.Unsafe 類下鸽斟,就有許多可以使用的 CAS 方法,這些方法都屬于 native 方法利诺,即最終實(shí)現(xiàn)由底層 C/C++ 完成富蓄,在 JUC 里面用的最多的應(yīng)該就是 compareAndSwapInt 方法。
//第一個(gè)參數(shù)是需要修改的對(duì)象
//第二個(gè)參數(shù)是這個(gè)對(duì)象需要修改的字段的內(nèi)存地址偏移量慢逾,用于直接找到相應(yīng)的內(nèi)存位置
//第三個(gè)參數(shù)是需要比較的值立倍,如果當(dāng)前內(nèi)存的值與這個(gè)值相同,才會(huì)進(jìn)行數(shù)據(jù)寫入
//第四個(gè)參數(shù)是需要寫入的值
public final native boolean compareAndSwapInt(Object var1, long offset, int expect, int update);
如果業(yè)務(wù)屬于CPU密集類型侣滩,一般我們會(huì)一直循環(huán)調(diào)用CAS來實(shí)現(xiàn)非阻塞資源競(jìng)爭口注,不需要通過鎖的形式來修改某個(gè)共享變量,從而減少線程切換導(dǎo)致的性能損耗君珠,但是這種做法可能會(huì)導(dǎo)致CPU的資源損耗寝志,因?yàn)樗姥h(huán)會(huì)一直占用 CPU 資源。
當(dāng)然策添,如果 CAS 能解決所有共享變量的線程安全問題材部,為什么我們還需要用鎖的形式呢?接下來我們來看看 CAS 的一些問題以及相應(yīng)的解決辦法:
- 自旋的CAS開銷大唯竹,如果線程一直競(jìng)爭資源失敗乐导,則會(huì)導(dǎo)致cpu一直自旋,造成不必要的開銷浸颓,如果業(yè)務(wù)允許放棄資源物臂,則可以通過設(shè)置超時(shí)時(shí)間解決.
- CAS只能保證一個(gè)共享變量的原子操作,如果我們想對(duì)多個(gè)共享變量進(jìn)行原子操作猾愿,只能通過鎖的形式解決.
- ABA問題鹦聪,如果ABA并沒有對(duì)業(yè)務(wù)有實(shí)質(zhì)上的影響的話,這個(gè)并不算問題蒂秘,當(dāng)如果業(yè)務(wù)想要了解該變量當(dāng)前的語義泽本,就會(huì)出現(xiàn)問題,例如棧的問題姻僧,存在棧A-B-C规丽,A是棧頂,有線程X想要將B改為棧頂撇贺,線程X獲取A的值時(shí)線程被掛起赌莺,同時(shí)線程Y介入,將棧A,B出棧松嘶,同時(shí)將A入棧艘狭,這個(gè)時(shí)候棧結(jié)構(gòu)為A-C,線程X喚醒,此時(shí)他對(duì)棧進(jìn)行出棧操作巢音,并將B作為棧頂遵倦,但由于B已經(jīng)出棧,B.next為null官撼,這就導(dǎo)致了C的丟失梧躺,最終棧結(jié)構(gòu)變?yōu)锽而不是最初的想法B-C,這個(gè)問題可以通過加入時(shí)間戳來實(shí)現(xiàn)預(yù)期值變化的感知.
3. AQS 是什么傲绣,它有什么作用
在傳統(tǒng)的多線程資源競(jìng)爭當(dāng)中掠哥,這些線程并沒有進(jìn)行統(tǒng)一的管理,可能導(dǎo)致的結(jié)果便是部分線程永遠(yuǎn)搶占不到鎖秃诵,即饑餓問題续搀。
為了對(duì)多線程并發(fā)進(jìn)行一個(gè)統(tǒng)一管理,AQS 即 AbstractQueuedSynchronizer 誕生了顷链。
AQS 的本質(zhì)是通過一個(gè)雙向鏈表和一個(gè)狀態(tài)值的結(jié)構(gòu)來管理多個(gè)線程目代,鏈表里面存儲(chǔ)的是一個(gè)個(gè)封裝好的線程對(duì)象節(jié)點(diǎn)屈梁,而狀態(tài)值代表了當(dāng)前鎖的狀態(tài)嗤练,其中 AQS 會(huì)使用 CAS 進(jìn)行狀態(tài)值的修改,從而保證當(dāng)前鎖的狀態(tài)值無誤在讶。
接下來我們通過一系列的圖片煞抬,來了解下 AQS 的基本原理。
首先我們來看看 AQS 的結(jié)構(gòu)是怎么樣的:
AQS的結(jié)構(gòu)非常簡單残拐,接著我們借助 ReentrantLock 來了解下 AQS 如何管理線程:這里有個(gè)關(guān)鍵點(diǎn)构哺,即 head 節(jié)點(diǎn)本質(zhì)上不算是阻塞隊(duì)列的一員革答,因?yàn)樵摴?jié)點(diǎn)的線程是持有鎖的,后續(xù)節(jié)點(diǎn)的喚醒都需要通過 head 節(jié)點(diǎn)來實(shí)現(xiàn)曙强,而這個(gè) head 節(jié)點(diǎn)里面封裝的線程具體是什么也不重要
以下操作中,線程顏色對(duì)應(yīng)文字顏色碟嘴,即流程文字對(duì)應(yīng)相應(yīng)的線程操作
為了預(yù)防當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)被中斷取消導(dǎo)致無法喚醒當(dāng)前節(jié)點(diǎn)溪食,在 shouldParkAfterFailedAcquire() 方法中會(huì)通過遍歷的形式尋找一個(gè)"有效的前驅(qū)節(jié)點(diǎn)"進(jìn)行關(guān)聯(lián)并設(shè)置這個(gè)前驅(qū)節(jié)點(diǎn)的 waitStatus 值
接下來我們?cè)诳焖龠^一遍入隊(duì)過程:可以看出,AQS 入隊(duì)操作主要分為 3 個(gè)步驟娜扇,分別是:
- 競(jìng)爭鎖错沃,競(jìng)爭成功直接持有鎖,失敗則進(jìn)入步驟 2 雀瓢;如果當(dāng)前是公平鎖并且阻塞隊(duì)列的 tail 不為空枢析,則直接進(jìn)入步驟 2 而不進(jìn)行競(jìng)爭
- 將當(dāng)前線程包裝為一個(gè) Node 節(jié)點(diǎn),如果隊(duì)列沒初始化則先進(jìn)行初始化操作刃麸,等到阻塞隊(duì)列的 tail 不為空醒叁,則將 tail 賦值為當(dāng)前的 Node 節(jié)點(diǎn),并與上任 tail 節(jié)點(diǎn)進(jìn)行關(guān)聯(lián)
- 如果當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)為 head ,則嘗試競(jìng)爭鎖把沼,競(jìng)爭失敗則將當(dāng)前節(jié)點(diǎn)的有效前驅(qū)節(jié)點(diǎn)的waitStatus設(shè)置為-1断傲,最后進(jìn)入阻塞狀態(tài),等待前驅(qū)節(jié)點(diǎn)的喚醒
這里為什么要從尾部往前遍歷呢? 主要原因是防止中間有節(jié)點(diǎn)已經(jīng)處于取消狀態(tài)并且與它的后續(xù)節(jié)點(diǎn)斷開關(guān)聯(lián)導(dǎo)致無法喚醒阻塞隊(duì)列其他的等待節(jié)點(diǎn)认罩,因此從后往前遍歷一個(gè)最接近 head 節(jié)點(diǎn)的阻塞節(jié)點(diǎn)進(jìn)行喚醒
AQS 的出隊(duì)以及喚醒操作主要分為 3 個(gè)步驟:
- 持有鎖的線程修改 state 字段以及 exclusiveOwnerThread 字段(不考慮重入)
- 如果當(dāng)前線程節(jié)點(diǎn)的 waitStatus 不為0,從后往前遍歷尋找最接近 head 的節(jié)點(diǎn)并將該節(jié)點(diǎn)的線程喚醒
- 被喚醒的線程先將前驅(qū)節(jié)點(diǎn)設(shè)置為 head (如果已經(jīng)是 head 則跳過該步驟)续捂,然后進(jìn)行鎖的競(jìng)爭(非公平鎖的話有可能競(jìng)爭失敗然后進(jìn)入阻塞)垦垂,競(jìng)爭成功后修改 state 字段以及 exclusiveOwnerThread 字段,并將 head 賦值為當(dāng)前節(jié)點(diǎn)
AQS 使用了模板設(shè)計(jì)模式牙瓢,將阻塞隊(duì)列的隊(duì)列操作自行實(shí)現(xiàn)劫拗,然后給子類留下了 tryAcquire(競(jìng)爭鎖)/tryRelease(釋放鎖)等模板方法,例如自定義公平鎖或非公平鎖的實(shí)現(xiàn)矾克,子類通過繼承 AQS 來實(shí)現(xiàn)具體的上鎖\釋放鎖的方法页慷,因此 AQS 的作用是為同步工具類提供基于阻塞隊(duì)列的組件。
同時(shí)胁附,為了保證 AQS 在多線程進(jìn)行入隊(duì)-出隊(duì)操作時(shí)的線程安全操作酒繁, AQS 內(nèi)部使用了大量的自旋 CAS 操作保證線程安全,并利用了 LockSupport 類的 park() 和 unpark() 方法進(jìn)行線程的阻塞和釋放控妻。
AQS 典型的實(shí)現(xiàn)類有 ReentrantLock 州袒,它通過一個(gè)內(nèi)部類 FairSync/NonfairSync 繼承 AQS 定義了公平鎖/非公平鎖競(jìng)爭鎖的具體邏輯。搞懂了 AQS 弓候,在看 JUC 包內(nèi)的同步工具類便會(huì)變得很簡單郎哭,因?yàn)槟阒恍枰P(guān)注他們不同的上鎖/釋放鎖的邏輯即可,至于線程的管理菇存,幾乎都是一樣夸研。
總結(jié)
在該篇章中,我首先介紹了帶有返回值的多線程實(shí)現(xiàn)方法依鸥,其次介紹了在同步容器中經(jīng)常出現(xiàn)的 CAS 亥至,最后介紹了同步容器的核心類 AQS ,其中 AQS 的內(nèi)容可能偏多毕籽,建議讀者在閱覽 AQS 流程圖解的時(shí)候可以關(guān)聯(lián)下具體代碼的實(shí)現(xiàn)抬闯,可以有效幫助理解 AQS 的運(yùn)行流程和作用。
接下來关筒,我們回到最初的三個(gè)問題
1. 實(shí)現(xiàn)多線程的方式有哪些溶握,ThreadLocal使用中需要注意什么?
2. CAS 是什么蒸播?CAS 有什么缺陷睡榆,如何解決萍肆?
3. AQS 是什么,它有什么作用胀屿?
如果你們可以很流暢的回答這些問題塘揣,那么恭喜你,該章節(jié)的內(nèi)容已經(jīng)全部掌握宿崭,如果不行亲铡,希望可以回到對(duì)應(yīng)問題講解的地方,或者對(duì)某個(gè)不了解的點(diǎn)進(jìn)行額外的知識(shí)搜索葡兑,盡量用自己組織的語言回答這些問題奖蔓。