兩萬字!多線程50問!
前言
大家好,我整理了50道多線程并發(fā)面試題,大家可以點(diǎn)贊、收藏起來,慢慢品!~
1、為什么要使用多線程
選擇多線程的原因,就是因?yàn)榭臁Ee個(gè)例子:
如果要把1000塊磚搬到樓頂,假設(shè)到樓頂有幾個(gè)電梯,你覺得用一個(gè)電梯搬運(yùn)快,還是同時(shí)用幾個(gè)電梯同時(shí)搬運(yùn)快呢?這個(gè)電梯就可以理解為線程。

所以,我們使用多線程就是因?yàn)椋?在正確的場(chǎng)景下,設(shè)置恰當(dāng)數(shù)目的線程,可以用來程提高序的運(yùn)行速率。更專業(yè)點(diǎn)講,就是充分地利用CPU和I/O的利用率,提升程序運(yùn)行速率。
當(dāng)然,有利就有弊,多線程場(chǎng)景下,我們要保證線程安全,就需要考慮加鎖。加鎖如果不恰當(dāng),就很很耗性能。
2. 創(chuàng)建線程有幾種方式?

Java中創(chuàng)建線程主要有以下這幾種方式:
定義
Thread
類的子類,并重寫該類的run
方法定義
Runnable
接口的實(shí)現(xiàn)類,并重寫該接口的run()
方法定義
Callable
接口的實(shí)現(xiàn)類,并重寫該接口的call()
方法,一般配合Future
使用線程池的方式
2.1 定義Thread類的子類,并重寫該類的run方法
復(fù)制代碼
2.2 定義Runnable接口的實(shí)現(xiàn)類,并重寫該接口的run()方法
2.3 定義Callable接口的實(shí)現(xiàn)類,并重寫該接口的call()方法
如果想要執(zhí)行的線程有返回,可以使用Callable
。
2.4 線程池的方式
日常開發(fā)中,我們一般都是用線程池的方式執(zhí)行異步任務(wù)。
3. start()方法和run()方法的區(qū)別
其實(shí)start
和run
的主要區(qū)別如下:
start
方法可以啟動(dòng)一個(gè)新線程,run
方法只是類的一個(gè)普通方法而已,如果直接調(diào)用run
方法,程序中依然只有主線程這一個(gè)線程。start
方法實(shí)現(xiàn)了多線程,而run
方法沒有實(shí)現(xiàn)多線程。start
不能被重復(fù)調(diào)用,而run
方法可以。start
方法中的run
代碼可以不執(zhí)行完,就繼續(xù)執(zhí)行下面的代碼,也就是說進(jìn)行了線程切換。然而,如果直接調(diào)用run
方法,就必須等待其代碼全部執(zhí)行完才能繼續(xù)執(zhí)行下面的代碼。
大家可以結(jié)合代碼例子來看看哈~
4. 線程和進(jìn)程的區(qū)別
進(jìn)程是運(yùn)行中的應(yīng)用程序,線程是進(jìn)程的內(nèi)部的一個(gè)執(zhí)行序列
進(jìn)程是資源分配的最小單位,線程是CPU調(diào)度的最小單位。
一個(gè)進(jìn)程可以有多個(gè)線程。線程又叫做輕量級(jí)進(jìn)程,多個(gè)線程共享進(jìn)程的資源
進(jìn)程間切換代價(jià)大,線程間切換代價(jià)小
進(jìn)程擁有資源多,線程擁有資源少地址
進(jìn)程是存在地址空間的,而線程本身無地址空間,線程的地址空間是包含在進(jìn)程中的
舉個(gè)例子:
你打開QQ,開了一個(gè)進(jìn)程;打開了迅雷,也開了一個(gè)進(jìn)程。
在QQ的這個(gè)進(jìn)程里,傳輸文字開一個(gè)線程、傳輸語音開了一個(gè)線程、彈出對(duì)話框又開了一個(gè)線程。
所以運(yùn)行某個(gè)軟件,相當(dāng)于開了一個(gè)進(jìn)程。在這個(gè)軟件運(yùn)行的過程里(在這個(gè)進(jìn)程里),多個(gè)工作支撐的完成QQ的運(yùn)行,那么這“多個(gè)工作”分別有一個(gè)線程。
所以一個(gè)進(jìn)程管著多個(gè)線程。
通俗的講:“進(jìn)程是爹媽,管著眾多的線程兒子”...
5. 說一下 Runnable和 Callable有什么區(qū)別?
Runnable
接口中的run()
方法沒有返回值,是void
類型,它做的事情只是純粹地去執(zhí)行run()
方法中的代碼而已;Callable
接口中的call()
方法是有返回值的,是一個(gè)泛型。它一般配合Future、FutureTask
一起使用,用來獲取異步執(zhí)行的結(jié)果。Callable
接口call()
方法允許拋出異常;而Runnable
接口run()
方法不能繼續(xù)上拋異常;
大家可以看下它倆的API
:
為了方便大家理解,寫了一個(gè)demo,小伙伴們可以看看哈:
6. 聊聊volatile作用,原理
volatile關(guān)鍵字是Java虛擬機(jī)提供的的最輕量級(jí)的同步機(jī)制。它作為一個(gè)修飾符,用來修飾變量。它保證變量對(duì)所有線程可見性,禁止指令重排,但是不保證原子性。
我們先來一起回憶下java內(nèi)存模型(jmm):
Java虛擬機(jī)規(guī)范試圖定義一種Java內(nèi)存模型,來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)上都能達(dá)到一致的內(nèi)存訪問效果。
Java內(nèi)存模型規(guī)定所有的變量都是存在主內(nèi)存當(dāng)中,每個(gè)線程都有自己的工作內(nèi)存。這里的變量包括實(shí)例變量和靜態(tài)變量,但是不包括局部變量,因?yàn)榫植孔兞渴蔷€程私有的。
線程的工作內(nèi)存保存了被該線程使用的變量的主內(nèi)存副本,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接操作主內(nèi)存。并且每個(gè)線程不能訪問其他線程的工作內(nèi)存。

volatile變量,保證新值能立即同步回主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新,所以我們說volatile保證了多線程操作變量的可見性。
volatile保證可見性和禁止指令重排,都跟內(nèi)存屏障有關(guān)。我們來看一段volatile使用的demo代碼:
編譯后,對(duì)比有volatile
關(guān)鍵字和沒有volatile
關(guān)鍵字時(shí)所生成的匯編代碼,發(fā)現(xiàn)有volatile
關(guān)鍵字修飾時(shí),會(huì)多出一個(gè)lock addl $0x0,(%esp)
,即多出一個(gè)lock前綴指令,lock指令相當(dāng)于一個(gè)內(nèi)存屏障
lock
指令相當(dāng)于一個(gè)內(nèi)存屏障,它保證以下這幾點(diǎn):
重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置
將本處理器的緩存寫入內(nèi)存
如果是寫入動(dòng)作,會(huì)導(dǎo)致其他處理器中對(duì)應(yīng)的緩存無效。
第2點(diǎn)和第3點(diǎn)就是保證volatile
保證可見性的體現(xiàn)嘛,第1點(diǎn)就是禁止指令重排的體現(xiàn)。
內(nèi)存屏障四大分類:(Load 代表讀取指令,Store代表寫入指令)

在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
有些小伙伴,可能對(duì)這個(gè)還是有點(diǎn)疑惑,內(nèi)存屏障這玩意太抽象了。我們照著代碼看下吧:

內(nèi)存屏障保證前面的指令先執(zhí)行,所以這就保證了禁止了指令重排啦,同時(shí)內(nèi)存屏障保證緩存寫入內(nèi)存和其他處理器緩存失效,這也就保證了可見性,哈哈有關(guān)于volatile的底層實(shí)現(xiàn),我們就討論到這哈
7. 說說并發(fā)與并行的區(qū)別?
并發(fā)和并行最開始都是操作系統(tǒng)中的概念,表示的是CPU執(zhí)行多個(gè)任務(wù)的方式。
順序:上一個(gè)開始執(zhí)行的任務(wù)完成后,當(dāng)前任務(wù)才能開始執(zhí)行
并發(fā):無論上一個(gè)開始執(zhí)行的任務(wù)是否完成,當(dāng)前任務(wù)都可以開始執(zhí)行
(即 A B 順序執(zhí)行的話,A 一定會(huì)比 B 先完成,而并發(fā)執(zhí)行則不一定。)
串行:有一個(gè)任務(wù)執(zhí)行單元,從物理上就只能一個(gè)任務(wù)、一個(gè)任務(wù)地執(zhí)行
并行:有多個(gè)任務(wù)執(zhí)行單元,從物理上就可以多個(gè)任務(wù)一起執(zhí)行
(即在任意時(shí)間點(diǎn)上,串行執(zhí)行時(shí)必然只有一個(gè)任務(wù)在執(zhí)行,而并行則不一定。)
知乎有個(gè)很有意思的回答,大家可以看下:
你吃飯吃到一半,電話來了,你一直到吃完了以后才去接,這就說明你不支持并發(fā)也不支持并行。
你吃飯吃到一半,電話來了,你停了下來接了電話,接完后繼續(xù)吃飯,這說明你支持并發(fā)。
你吃飯吃到一半,電話來了,你一邊打電話一邊吃飯,這說明你支持并行。
并發(fā)的關(guān)鍵是你有處理多個(gè)任務(wù)的能力,不一定要同時(shí)。并行的關(guān)鍵是你有同時(shí)處理多個(gè)任務(wù)的能力。所以我認(rèn)為它們最關(guān)鍵的點(diǎn)就是:是否是同時(shí)。

8.synchronized 的實(shí)現(xiàn)原理以及鎖優(yōu)化?
synchronized是Java中的關(guān)鍵字,是一種同步鎖。synchronized關(guān)鍵字可以作用于方法或者代碼塊。
一般面試時(shí)??梢赃@么回答:

8.1 monitorenter、monitorexit、ACC_SYNCHRONIZED
如果synchronized作用于代碼塊,反編譯可以看到兩個(gè)指令:monitorenter、monitorexit
,JVM使用monitorenter和monitorexit
兩個(gè)指令實(shí)現(xiàn)同步;如果作用synchronized作用于方法,反編譯可以看到ACCSYNCHRONIZED標(biāo)記
,JVM通過在方法訪問標(biāo)識(shí)符(flags)中加入ACCSYNCHRONIZED
來實(shí)現(xiàn)同步功能。
同步代碼塊是通過
monitorenter和monitorexit
來實(shí)現(xiàn),當(dāng)線程執(zhí)行到monitorenter的時(shí)候要先獲得monitor鎖,才能執(zhí)行后面的方法。當(dāng)線程執(zhí)行到monitorexit的時(shí)候則要釋放鎖。同步方法是通過中設(shè)置ACCSYNCHRONIZED標(biāo)志來實(shí)現(xiàn),當(dāng)線程執(zhí)行有ACCSYNCHRONI標(biāo)志的方法,需要獲得monitor鎖。每個(gè)對(duì)象都與一個(gè)monitor相關(guān)聯(lián),線程可以占有或者釋放monitor。
8.2 monitor監(jiān)視器
monitor是什么呢?操作系統(tǒng)的管程(monitors)是概念原理,ObjectMonitor是它的原理實(shí)現(xiàn)。

在Java虛擬機(jī)(HotSpot)中,Monitor(管程)是由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下:
ObjectMonitor中幾個(gè)關(guān)鍵字段的含義如圖所示:

8.3 Java Monitor 的工作機(jī)理

想要獲取monitor的線程,首先會(huì)進(jìn)入_EntryList隊(duì)列。
當(dāng)某個(gè)線程獲取到對(duì)象的monitor后,進(jìn)入Owner區(qū)域,設(shè)置為當(dāng)前線程,同時(shí)計(jì)數(shù)器count加1。
如果線程調(diào)用了wait()方法,則會(huì)進(jìn)入WaitSet隊(duì)列。它會(huì)釋放monitor鎖,即將owner賦值為null,count自減1,進(jìn)入WaitSet隊(duì)列阻塞等待。
如果其他線程調(diào)用 notify() / notifyAll() ,會(huì)喚醒WaitSet中的某個(gè)線程,該線程再次嘗試獲取monitor鎖,成功即進(jìn)入Owner區(qū)域。
同步方法執(zhí)行完畢了,線程退出臨界區(qū),會(huì)將monitor的owner設(shè)為null,并釋放監(jiān)視鎖。
8.4 對(duì)象與monitor關(guān)聯(lián)

在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header),實(shí)例數(shù)據(jù)(Instance Data)和對(duì)象填充(Padding)。
對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Class Pointer(類型指針)。
Mark Word 是用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等。

重量級(jí)鎖,指向互斥量的指針。其實(shí)synchronized是重量級(jí)鎖,也就是說Synchronized的對(duì)象鎖,Mark Word鎖標(biāo)識(shí)位為10,其中指針指向的是Monitor對(duì)象的起始地址。
9. 線程有哪些狀態(tài)?
線程有6個(gè)狀態(tài),分別是:New, Runnable, Blocked, Waiting, Timed_Waiting, Terminated
。
轉(zhuǎn)換關(guān)系圖如下:

New:線程對(duì)象創(chuàng)建之后、但還沒有調(diào)用
start()
方法,就是這個(gè)狀態(tài)。
Runnable:它包括就緒(
ready
)和運(yùn)行中(running
)兩種狀態(tài)。如果調(diào)用start
方法,線程就會(huì)進(jìn)入Runnable
狀態(tài)。它表示我這個(gè)線程可以被執(zhí)行啦(此時(shí)相當(dāng)于ready
狀態(tài)),如果這個(gè)線程被調(diào)度器分配了CPU時(shí)間,那么就可以被執(zhí)行(此時(shí)處于running
狀態(tài))。
Blocked: 阻塞的(被同步鎖或者IO鎖阻塞)。表示線程阻塞于鎖,線程阻塞在進(jìn)入
synchronized
關(guān)鍵字修飾的方法或代碼塊(等待獲取鎖)時(shí)的狀態(tài)。比如前面有一個(gè)臨界區(qū)的代碼需要執(zhí)行,那么線程就需要等待,它就會(huì)進(jìn)入這個(gè)狀態(tài)。它一般是從RUNNABLE
狀態(tài)轉(zhuǎn)化過來的。如果線程獲取到鎖,它將變成RUNNABLE
狀態(tài)
WAITING : 永久等待狀態(tài),進(jìn)入該狀態(tài)的線程需要等待其他線程做出一些特定動(dòng)作(比如通知)。處于該狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,它們要等待被顯式地喚醒,否則會(huì)處于無限期等待的狀態(tài)。一般
Object.wait
。
TIMED_WATING: 等待指定的時(shí)間重新被喚醒的狀態(tài)。有一個(gè)計(jì)時(shí)器在里面計(jì)算的,最常見就是使用
Thread.sleep
方法觸發(fā),觸發(fā)后,線程就進(jìn)入了Timed_waiting
狀態(tài),隨后會(huì)由計(jì)時(shí)器觸發(fā),再進(jìn)入Runnable
狀態(tài)。
?
終止(TERMINATED):表示該線程已經(jīng)執(zhí)行完成。
再來看個(gè)代碼demo吧:
10. synchronized和ReentrantLock的區(qū)別?
Synchronized
是依賴于JVM
實(shí)現(xiàn)的,而ReenTrantLock
是API
實(shí)現(xiàn)的。在
Synchronized
優(yōu)化以前,synchronized
的性能是比ReenTrantLock
差很多的,但是自從Synchronized
引入了偏向鎖,輕量級(jí)鎖(自旋鎖)后,兩者性能就差不多了。Synchronized
的使用比較方便簡(jiǎn)潔,它由編譯器去保證鎖的加鎖和釋放。而ReenTrantLock
需要手工聲明來加鎖和釋放鎖,最好在finally中聲明釋放鎖。ReentrantLock
可以指定是公平鎖還是?公平鎖。?synchronized
只能是?公平鎖。ReentrantLock
可響應(yīng)中斷、可輪回,而Synchronized
是不可以響應(yīng)中斷的
11. wait(),notify()和suspend(),resume()之間的區(qū)別
wait()
方法使得線程進(jìn)入阻塞等待狀態(tài),并且釋放鎖notify()
喚醒一個(gè)處于等待狀態(tài)的線程,它一般跟wait()
方法配套使用。suspend()
使得線程進(jìn)入阻塞狀態(tài),并且不會(huì)自動(dòng)恢復(fù),必須對(duì)應(yīng)的resume()
被調(diào)用,才能使得線程重新進(jìn)入可執(zhí)行狀態(tài)。suspend()
方法很容易引起死鎖問題。resume()
方法跟suspend()
方法配套使用。
suspend()不建議使用,因?yàn)?code>suspend()方法在調(diào)用后,線程不會(huì)釋放已經(jīng)占有的資 源(比如鎖),而是占有著資源進(jìn)入睡眠狀態(tài),這樣容易引發(fā)死鎖問題。
12. CAS?CAS 有什么缺陷,如何解決?
CAS
,全稱是Compare and Swap
,翻譯過來就是比較并交換;
CAS
涉及3個(gè)操作數(shù),內(nèi)存地址值V,預(yù)期原值A(chǔ),新值B;如果內(nèi)存位置的值V與預(yù)期原A值相匹配,就更新為新值B,否則不更新
CAS有什么缺陷?

ABA 問題
并發(fā)環(huán)境下,假設(shè)初始條件是A,去修改數(shù)據(jù)時(shí),發(fā)現(xiàn)是A就會(huì)執(zhí)行修改。但是看到的雖然是A,中間可能發(fā)生了A變B,B又變回A的情況。此時(shí)A已經(jīng)非彼A,數(shù)據(jù)即使成功修改,也可能有問題。
可以通過AtomicStampedReference
解決ABA問題,它,一個(gè)帶有標(biāo)記的原子引用類,通過控制變量值的版本來保證CAS的正確性。
循環(huán)時(shí)間長(zhǎng)開銷
自旋CAS,如果一直循環(huán)執(zhí)行,一直不成功,會(huì)給CPU帶來非常大的執(zhí)行開銷。很多時(shí)候,CAS思想體現(xiàn),是有個(gè)自旋次數(shù)的,就是為了避開這個(gè)耗時(shí)問題~
只能保證一個(gè)變量的原子操作。
CAS 保證的是對(duì)一個(gè)變量執(zhí)行操作的原子性,如果對(duì)多個(gè)變量操作時(shí),CAS 目前無法直接保證操作的原子性的??梢酝ㄟ^這兩個(gè)方式解決這個(gè)問題:1. 使用互斥鎖來保證原子性; 2.將多個(gè)變量封裝成對(duì)象,通過AtomicReference來保證原子性。
有興趣的朋友可以看看我之前的這篇實(shí)戰(zhàn)文章哈~CAS樂觀鎖解決并發(fā)問題的一次實(shí)踐
13. 說說CountDownLatch與CyclicBarrier 區(qū)別
CountDownLatch和CyclicBarrier
都用于讓線程等待,達(dá)到一定條件時(shí)再運(yùn)行。主要區(qū)別是:
CountDownLatch:一個(gè)或者多個(gè)線程,等待其他多個(gè)線程完成某件事情之后才能執(zhí)行;
CyclicBarrier:多個(gè)線程互相等待,直到到達(dá)同一個(gè)同步點(diǎn),再繼續(xù)一起執(zhí)行。

舉個(gè)例子吧:
CountDownLatch:假設(shè)老師跟同學(xué)約定周末在公園門口集合,等人齊了再發(fā)門票。那么,發(fā)門票(這個(gè)主線程),需要等各位同學(xué)都到齊(多個(gè)其他線程都完成),才能執(zhí)行。
CyclicBarrier:多名短跑運(yùn)動(dòng)員要開始田徑比賽,只有等所有運(yùn)動(dòng)員準(zhǔn)備好,裁判才會(huì)鳴槍開始,這時(shí)候所有的運(yùn)動(dòng)員才會(huì)疾步如飛。
14. 什么是多線程環(huán)境下的偽共享
14.1 什么是偽共享?
CPU的緩存是以緩存行(cache line)為單位進(jìn)行緩存的,當(dāng)多個(gè)線程修改相互獨(dú)立的變量,而這些變量又處于同一個(gè)緩存行時(shí)就會(huì)影響彼此的性能。這就是偽共享
現(xiàn)代計(jì)算機(jī)計(jì)算模型:

CPU執(zhí)行速度比內(nèi)存速度快好幾個(gè)數(shù)量級(jí),為了提高執(zhí)行效率,現(xiàn)代計(jì)算機(jī)模型演變出CPU、緩存(L1,L2,L3),內(nèi)存的模型。
CPU執(zhí)行運(yùn)算時(shí),如先從L1緩存查詢數(shù)據(jù),找不到再去L2緩存找,依次類推,直到在內(nèi)存獲取到數(shù)據(jù)。
為了避免頻繁從內(nèi)存獲取數(shù)據(jù),聰明的科學(xué)家設(shè)計(jì)出緩存行,緩存行大小為64字節(jié)。
也正是因?yàn)?strong>緩存行的存在,就導(dǎo)致了偽共享問題,如圖所示:

假設(shè)數(shù)據(jù)a、b
被加載到同一個(gè)緩存行。
當(dāng)線程1修改了a的值,這時(shí)候CPU1就會(huì)通知其他CPU核,當(dāng)前緩存行(Cache line)已經(jīng)失效。
這時(shí)候,如果線程2發(fā)起修改b,因?yàn)榫彺嫘幸呀?jīng)失效了,所以「core2 這時(shí)會(huì)重新從主內(nèi)存中讀取該 Cache line 數(shù)據(jù)」。讀完后,因?yàn)樗薷腷的值,那么CPU2就通知其他CPU核,當(dāng)前緩存行(Cache line)又已經(jīng)失效。
醬紫,如果同一個(gè)Cache line的內(nèi)容被多個(gè)線程讀寫,就很容易產(chǎn)生相互競(jìng)爭(zhēng),頻繁回寫主內(nèi)存,會(huì)大大降低性能。
14.2 如何解決偽共享問題
既然偽共享是因?yàn)橄嗷オ?dú)立的變量存儲(chǔ)到相同的Cache line導(dǎo)致的,一個(gè)緩存行大小是64字節(jié)。那么,我們就可以使用空間換時(shí)間的方法,即數(shù)據(jù)填充的方式,把獨(dú)立的變量分散到不同的Cache line~
來看個(gè)例子:
一個(gè)long類型是8字節(jié),我們?cè)谧兞縜和b之間不上7個(gè)long類型變量呢,輸出結(jié)果是啥呢?如下:
可以發(fā)現(xiàn)利用填充數(shù)據(jù)的方式,讓讀寫的變量分割到不同緩存行,可以很好挺高性能~
15. Fork/Join框架的理解
Fork/Join框架是Java7提供的一個(gè)用于并行執(zhí)行任務(wù)的框架,是一個(gè)把大任務(wù)分割成若干個(gè)小任務(wù),最終匯總每個(gè)小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架。
Fork/Join框架需要理解兩個(gè)點(diǎn),「分而治之」和「工作竊取算法」。
分而治之
以上Fork/Join框架的定義,就是分而治之思想的體現(xiàn)啦

工作竊取算法
把大任務(wù)拆分成小任務(wù),放到不同隊(duì)列執(zhí)行,交由不同的線程分別執(zhí)行時(shí)。有的線程優(yōu)先把自己負(fù)責(zé)的任務(wù)執(zhí)行完了,其他線程還在慢慢悠悠處理自己的任務(wù),這時(shí)候?yàn)榱顺浞痔岣咝?,就需要工作盜竊算法啦~

工作盜竊算法就是,「某個(gè)線程從其他隊(duì)列中竊取任務(wù)進(jìn)行執(zhí)行的過程」。一般就是指做得快的線程(盜竊線程)搶慢的線程的任務(wù)來做,同時(shí)為了減少鎖競(jìng)爭(zhēng),通常使用雙端隊(duì)列,即快線程和慢線程各在一端。
16. 聊聊ThreadLocal原理?
ThreadLocal的內(nèi)存結(jié)構(gòu)圖
為了對(duì)ThreadLocal
有個(gè)宏觀的認(rèn)識(shí),我們先來看下ThreadLocal
的內(nèi)存結(jié)構(gòu)圖

從內(nèi)存結(jié)構(gòu)圖,我們可以看到:
Thread
類中,有個(gè)ThreadLocal.ThreadLocalMap
?的成員變量。ThreadLocalMap
內(nèi)部維護(hù)了Entry
數(shù)組,每個(gè)Entry
代表一個(gè)完整的對(duì)象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型對(duì)象值。
關(guān)鍵源碼分析
對(duì)照著關(guān)鍵源碼來看,更容易理解一點(diǎn)哈~
首先看下Thread
類的源碼,可以看到成員變量ThreadLocalMap
的初始值是為null
成員變量ThreadLocalMap
的關(guān)鍵源碼如下:
ThreadLocal
類中的關(guān)鍵set()
方法:
ThreadLocal
類中的關(guān)鍵get()
方法
所以怎么回答ThreadLocal的實(shí)現(xiàn)原理?如下,最好是能結(jié)合以上結(jié)構(gòu)圖一起說明哈~
Thread
線程類有一個(gè)類型為ThreadLocal.ThreadLocalMap
的實(shí)例變量threadLocals
,即每個(gè)線程都有一個(gè)屬于自己的ThreadLocalMap
。ThreadLocalMap
內(nèi)部維護(hù)著Entry
數(shù)組,每個(gè)Entry
代表一個(gè)完整的對(duì)象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型值。并發(fā)多線程場(chǎng)景下,每個(gè)線程
Thread
,在往ThreadLocal
里設(shè)置值的時(shí)候,都是往自己的ThreadLocalMap
里存,讀也是以某個(gè)ThreadLocal
作為引用,在自己的map
里找對(duì)應(yīng)的key
,從而可以實(shí)現(xiàn)了線程隔離。
17. TreadLocal為什么會(huì)導(dǎo)致內(nèi)存泄漏呢?
弱引用導(dǎo)致的內(nèi)存泄漏呢?
key是弱引用,GC回收會(huì)影響ThreadLocal的正常工作嘛?
ThreadLocal內(nèi)存泄漏的demo
17.1 弱引用導(dǎo)致的內(nèi)存泄漏呢?
我們先來看看TreadLocal
的引用示意圖哈:

關(guān)于ThreadLocal內(nèi)存泄漏,網(wǎng)上比較流行的說法是這樣的:
ThreadLocalMap
使用ThreadLocal
的弱引用作為key
,當(dāng)ThreadLocal
變量被手動(dòng)設(shè)置為null
,即一個(gè)ThreadLocal
沒有外部強(qiáng)引用來引用它,當(dāng)系統(tǒng)GC時(shí),ThreadLocal
一定會(huì)被回收。這樣的話,ThreadLocalMap
中就會(huì)出現(xiàn)key
為null
的Entry
,就沒有辦法訪問這些key
為null
的Entry
的value
,如果當(dāng)前線程再遲遲不結(jié)束的話(比如線程池的核心線程),這些key
為null
的Entry
的value
就會(huì)一直存在一條強(qiáng)引用鏈:Thread變量 -> Thread對(duì)象 -> ThreaLocalMap -> Entry -> value -> Object 永遠(yuǎn)無法回收,造成內(nèi)存泄漏。
當(dāng)ThreadLocal
變量被手動(dòng)設(shè)置為null
后的引用鏈圖:

實(shí)際上,ThreadLocalMap
的設(shè)計(jì)中已經(jīng)考慮到這種情況。所以也加上了一些防護(hù)措施:即在ThreadLocal
的get
,set
,remove
方法,都會(huì)清除線程ThreadLocalMap
里所有key
為null
的value
。
源代碼中,是有體現(xiàn)的,如ThreadLocalMap
的set
方法:
如ThreadLocal的get
方法:
17.2 key是弱引用,GC回收會(huì)影響ThreadLocal的正常工作嘛?
有些小伙伴可能有疑問,ThreadLocal
的key
既然是弱引用.會(huì)不會(huì)GC貿(mào)然把key
回收掉,進(jìn)而影響ThreadLocal
的正常使用?
弱引用:具有弱引用的對(duì)象擁有更短暫的生命周期。如果一個(gè)對(duì)象只有弱引用存在了,則下次GC將會(huì)回收掉該對(duì)象(不管當(dāng)前內(nèi)存空間足夠與否)
其實(shí)不會(huì)的,因?yàn)橛?code>ThreadLocal變量引用著它,是不會(huì)被GC回收的,除非手動(dòng)把ThreadLocal變量設(shè)置為null
,我們可以跑個(gè)demo來驗(yàn)證一下:
結(jié)論就是,小伙伴放下這個(gè)疑惑了,哈哈~
17.3 ThreadLocal內(nèi)存泄漏的demo
給大家來看下一個(gè)內(nèi)存泄漏的例子,其實(shí)就是用線程池,一直往里面放對(duì)象
運(yùn)行結(jié)果出現(xiàn)了OOM,tianLuoThreadLocal.remove();
加上后,則不會(huì)OOM
。
我們這里沒有手動(dòng)設(shè)置tianLuoThreadLocal
變量為null
,但是還是會(huì)內(nèi)存泄漏。因?yàn)槲覀兪褂昧司€程池,線程池有很長(zhǎng)的生命周期,因此線程池會(huì)一直持有tianLuoClass
對(duì)象的value
值,即使設(shè)置tianLuoClass = null;
引用還是存在的。這就好像,你把一個(gè)個(gè)對(duì)象object
放到一個(gè)list
列表里,然后再單獨(dú)把object
設(shè)置為null
的道理是一樣的,列表的對(duì)象還是存在的。
所以內(nèi)存泄漏就這樣發(fā)生啦,最后內(nèi)存是有限的,就拋出了OOM
了。如果我們加上threadLocal.remove();
,則不會(huì)內(nèi)存泄漏。為什么呢?因?yàn)?code>threadLocal.remove();會(huì)清除Entry
,源碼如下:
18 為什么ThreadLocalMap 的 key是弱引用,設(shè)計(jì)理念是?
通過閱讀ThreadLocal
的源碼,我們是可以看到Entry
的Key
是設(shè)計(jì)為弱引用的(ThreadLocalMap
使用ThreadLocal
的弱引用作為Key
的)。為什么要設(shè)計(jì)為弱引用呢?

我們先來回憶一下四種引用:
強(qiáng)引用:我們平時(shí)
new
了一個(gè)對(duì)象就是強(qiáng)引用,例如Object obj = new Object();
即使在內(nèi)存不足的情況下,JVM寧愿拋出OutOfMemory錯(cuò)誤也不會(huì)回收這種對(duì)象。軟引用:如果一個(gè)對(duì)象只具有軟引用,則內(nèi)存空間足夠,垃圾回收器就不會(huì)回收它;如果內(nèi)存空間不足了,就會(huì)回收這些對(duì)象的內(nèi)存。
弱引用:具有弱引用的對(duì)象擁有更短暫的生命周期。如果一個(gè)對(duì)象只有弱引用存在了,則下次GC將會(huì)回收掉該對(duì)象(不管當(dāng)前內(nèi)存空間足夠與否)。
虛引用:如果一個(gè)對(duì)象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時(shí)候都可能被垃圾回收器回收。虛引用主要用來跟蹤對(duì)象被垃圾回收器回收的活動(dòng)。
我們先來看看官方文檔,為什么要設(shè)計(jì)為弱引用:
我再把ThreadLocal的引用示意圖搬過來:

下面我們分情況討論:
如果
Key
使用強(qiáng)引用:當(dāng)ThreadLocal
的對(duì)象被回收了,但是ThreadLocalMap
還持有ThreadLocal
的強(qiáng)引用的話,如果沒有手動(dòng)刪除,ThreadLocal就不會(huì)被回收,會(huì)出現(xiàn)Entry的內(nèi)存泄漏問題。如果
Key
使用弱引用:當(dāng)ThreadLocal
的對(duì)象被回收了,因?yàn)?code>ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動(dòng)刪除,ThreadLocal也會(huì)被回收。value
則在下一次ThreadLocalMap
調(diào)用set,get,remove
的時(shí)候會(huì)被清除。
因此可以發(fā)現(xiàn),使用弱引用作為Entry
的Key
,可以多一層保障:弱引用ThreadLocal
不會(huì)輕易內(nèi)存泄漏,對(duì)應(yīng)的value
在下一次ThreadLocalMap
調(diào)用set,get,remove
的時(shí)候會(huì)被清除。
實(shí)際上,我們的內(nèi)存泄漏的根本原因是,不再被使用的Entry
,沒有從線程的ThreadLocalMap
中刪除。一般刪除不再使用的Entry
有這兩種方式:
一種就是,使用完
ThreadLocal
,手動(dòng)調(diào)用remove()
,把Entry從ThreadLocalMap
中刪除另外一種方式就是:
ThreadLocalMap
的自動(dòng)清除機(jī)制去清除過期Entry
.(ThreadLocalMap
的get(),set()
時(shí)都會(huì)觸發(fā)對(duì)過期Entry
的清除)
19. 如何保證父子線程間的共享ThreadLocal數(shù)據(jù)
我們知道ThreadLocal
是線程隔離的,如果我們希望父子線程共享數(shù)據(jù),如何做到呢?可以使用InheritableThreadLocal
。先來看看demo
:
可以發(fā)現(xiàn),在子線程中,是可以獲取到父線程的 InheritableThreadLocal
類型變量的值,但是不能獲取到 ThreadLocal
類型變量的值。
獲取不到ThreadLocal
類型的值,我們可以好理解,因?yàn)樗蔷€程隔離的嘛。InheritableThreadLocal
是如何做到的呢?原理是什么呢?
在Thread
類中,除了成員變量threadLocals
之外,還有另一個(gè)成員變量:inheritableThreadLocals
。它們兩類型是一樣的:
Thread
類的init
方法中,有一段初始化設(shè)置:
可以發(fā)現(xiàn),當(dāng)parent的inheritableThreadLocals
不為null
時(shí),就會(huì)將parent
的inheritableThreadLocals
,賦值給前線程的inheritableThreadLocals
。說白了,就是如果當(dāng)前線程的inheritableThreadLocals
不為null
,就從父線程哪里拷貝過來一個(gè)過來,類似于另外一個(gè)ThreadLocal
,但是數(shù)據(jù)從父線程那里來的。有興趣的小伙伴們可以在去研究研究源碼~
20. 如何保證多線程下 i++ 結(jié)果正確?

使用循環(huán)CAS,實(shí)現(xiàn)i++原子操作
使用鎖機(jī)制,實(shí)現(xiàn)i++原子操作
使用synchronized,實(shí)現(xiàn)i++原子操作
舉個(gè)簡(jiǎn)單的例子,如下:
運(yùn)行結(jié)果:
21. 如何檢測(cè)死鎖?怎么預(yù)防死鎖?死鎖四個(gè)必要條件
死鎖是指多個(gè)線程因競(jìng)爭(zhēng)資源而造成的一種互相等待的僵局。如圖感受一下:

死鎖的四個(gè)必要條件:
互斥:一次只有一個(gè)進(jìn)程可以使用一個(gè)資源。其他進(jìn)程不能訪問已分配給其他進(jìn)程的資源。
占有且等待:當(dāng)一個(gè)進(jìn)程在等待分配得到其他資源時(shí),其繼續(xù)占有已分配得到的資源。
非搶占:不能強(qiáng)行搶占進(jìn)程中已占有的資源。
循環(huán)等待:存在一個(gè)封閉的進(jìn)程鏈,使得每個(gè)資源至少占有此鏈中下一個(gè)進(jìn)程所需要的一個(gè)資源。
如何預(yù)防死鎖?
加鎖順序(線程按順序辦事)
加鎖時(shí)限 (線程請(qǐng)求所加上權(quán)限,超時(shí)就放棄,同時(shí)釋放自己占有的鎖)
死鎖檢測(cè)
22. 如果線程過多,會(huì)怎樣?
使用多線程可以提升程序性能。但是如果使用過多的線程,則適得其反。
過多的線程會(huì)影響程序的系統(tǒng)。
一方面,線程的啟動(dòng)和銷毀,都是需要開銷的。
其次,過多的并發(fā)線程也會(huì)導(dǎo)致共享有限資源的開銷增大。過多的線程,還會(huì)導(dǎo)致內(nèi)存泄漏,筆者在以前公司,看到一個(gè)生產(chǎn)問題:一個(gè)第三方的包是使用new Thread來實(shí)現(xiàn)的,使用完沒有恰當(dāng)回收銷毀,最后引發(fā)內(nèi)存泄漏問題。
因此,我們平時(shí)盡量使用線程池來管理線程。同時(shí)還需要設(shè)置恰當(dāng)?shù)木€程數(shù)。
23. 聊聊happens-before原則
在Java語言中,有一個(gè)先行發(fā)生原則(happens-before
)。它包括八大規(guī)則,如下:
程序次序規(guī)則:在一個(gè)線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
管程鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖額lock操作
volatile變量規(guī)則:對(duì)一個(gè)變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作
線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)一個(gè)動(dòng)作
線程終止規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè),我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測(cè)到線程已經(jīng)終止執(zhí)行
線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生
對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize()方法的開始
傳遞性:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
24. 如何實(shí)現(xiàn)兩個(gè)線程間共享數(shù)據(jù)
可以通過類變量直接將數(shù)據(jù)放到主存中
通過并發(fā)的數(shù)據(jù)結(jié)構(gòu)來存儲(chǔ)數(shù)據(jù)
使用volatile變量或者鎖
調(diào)用atomic類(如AtomicInteger)
25. LockSupport作用是?
LockSupport是一個(gè)工具類。它的主要作用是掛起和喚醒線程。該工具類是創(chuàng)建鎖和其他同步類的基礎(chǔ)。它的主要方法是
看個(gè)代碼的例子:
因?yàn)?code>thread線程內(nèi)部有休眠2秒的操作,所以unpark
方法的操作肯定先于park
方法的調(diào)用。為什么thread
線程最終仍然可以結(jié)束,是因?yàn)?code>park和unpark
會(huì)對(duì)每個(gè)線程維持一個(gè)許可證(布爾值)
26 ?線程池如何調(diào)優(yōu),如何確認(rèn)最佳線程數(shù)?
我們的服務(wù)器CPU核數(shù)為8核,一個(gè)任務(wù)線程cpu耗時(shí)為20ms,線程等待(網(wǎng)絡(luò)IO、磁盤IO)耗時(shí)80ms,那最佳線程數(shù)目:( 80 + 20 )/20 * 8 = 40。也就是設(shè)置 40個(gè)線程數(shù)最佳。
27. 為什么要用線程池?
線程池:一個(gè)管理線程的池子。線程池可以:
管理線程,避免增加創(chuàng)建線程和銷毀線程的資源損耗。
提高響應(yīng)速度。
重復(fù)利用線程。
28. Java的線程池執(zhí)行原理
線程池的執(zhí)行原理如下:

為了形象描述線程池執(zhí)行,打個(gè)比喻:
核心線程比作公司正式員工
非核心線程比作外包員工
阻塞隊(duì)列比作需求池
提交任務(wù)比作提需求

29. 聊聊線程池的核心參數(shù)
我們先來看看ThreadPoolExecutor
的構(gòu)造函數(shù)
corePoolSize
:線程池核心線程數(shù)最大值maximumPoolSize
:線程池最大線程數(shù)大小keepAliveTime
:線程池中非核心線程空閑的存活時(shí)間大小unit
:線程空閑存活時(shí)間單位workQueue
:存放任務(wù)的阻塞隊(duì)列threadFactory
:用于設(shè)置創(chuàng)建線程的工廠,可以給創(chuàng)建的線程設(shè)置有意義的名字,可方便排查問題。handler
:線城池的飽和策略事件,主要有四種類型拒絕策略。
四種拒絕策略
AbortPolicy
(拋出一個(gè)異常,默認(rèn)的)DiscardPolicy
(直接丟棄任務(wù))DiscardOldestPolicy
(丟棄隊(duì)列里最老的任務(wù),將當(dāng)前這個(gè)任務(wù)繼續(xù)提交給線程池)CallerRunsPolicy
(交給線程池調(diào)用所在的線程進(jìn)行處理)
幾種工作阻塞隊(duì)列
ArrayBlockingQueue
(用數(shù)組實(shí)現(xiàn)的有界阻塞隊(duì)列,按FIFO排序量)LinkedBlockingQueue
(基于鏈表結(jié)構(gòu)的阻塞隊(duì)列,按FIFO排序任務(wù),容量可以選擇進(jìn)行設(shè)置,不設(shè)置的話,將是一個(gè)無邊界的阻塞隊(duì)列)DelayQueue
(一個(gè)任務(wù)定時(shí)周期的延遲執(zhí)行的隊(duì)列)PriorityBlockingQueue
(具有優(yōu)先級(jí)的無界阻塞隊(duì)列)SynchronousQueue
(一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列,每個(gè)插入操作必須等到另一個(gè)線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài))
30.當(dāng)提交新任務(wù)時(shí),異常如何處理?
我們先來看一段代碼:
顯然,這段代碼會(huì)有異常,我們?cè)賮砜纯磮?zhí)行結(jié)果

雖然沒有結(jié)果輸出,但是沒有拋出異常,所以我們無法感知任務(wù)出現(xiàn)了異常,所以需要添加try/catch。 如下圖:

OK,線程的異常處理,我們可以直接try...catch
捕獲。
最近寫了一篇線程池坑相關(guān)的,大家可以去看看哈: 細(xì)數(shù)線程池的10個(gè)坑
31. AQS組件,實(shí)現(xiàn)原理
AQS,即AbstractQueuedSynchronizer
,是構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架,它使用了一個(gè)int
成員變量表示同步狀態(tài),通過內(nèi)置的FIFO隊(duì)列來完成資源獲取線程的排隊(duì)工作。可以回答以下這幾個(gè)關(guān)鍵點(diǎn)哈:
state 狀態(tài)的維護(hù)。
CLH隊(duì)列
ConditionObject通知
模板方法設(shè)計(jì)模式
獨(dú)占與共享模式。
自定義同步器。
AQS全家桶的一些延伸,如:ReentrantLock等。
31.1 state 狀態(tài)的維護(hù)
state,int變量,鎖的狀態(tài),用volatile修飾,保證多線程中的可見性。
getState()和setState()方法采用final修飾,限制AQS的子類重寫它們兩。
compareAndSetState()方法采用樂觀鎖思想的CAS算法操作確保線程安全,保證狀態(tài) 設(shè)置的原子性。
31.2 CLH隊(duì)列

CLH 同步隊(duì)列,全英文Craig, Landin, and Hagersten locks
。是一個(gè)FIFO雙向隊(duì)列,其內(nèi)部通過節(jié)點(diǎn)head和tail記錄隊(duì)首和隊(duì)尾元素,隊(duì)列元素的類型為Node。AQS依賴它來完成同步狀態(tài)state的管理,當(dāng)前線程如果獲取同步狀態(tài)失敗時(shí),AQS則會(huì)將當(dāng)前線程已經(jīng)等待狀態(tài)等信息構(gòu)造成一個(gè)節(jié)點(diǎn)(Node)并將其加入到CLH同步隊(duì)列,同時(shí)會(huì)阻塞當(dāng)前線程,當(dāng)同步狀態(tài)釋放時(shí),會(huì)把首節(jié)點(diǎn)喚醒(公平鎖),使其再次嘗試獲取同步狀態(tài)。
31.3 ConditionObject通知
我們都知道,synchronized控制同步的時(shí)候,可以配合Object的wait()、notify(),notifyAll() 系列方法可以實(shí)現(xiàn)等待/通知模式。而Lock呢?它提供了條件Condition接口,配合await(),signal(),signalAll() 等方法也可以實(shí)現(xiàn)等待/通知機(jī)制。ConditionObject實(shí)現(xiàn)了Condition接口,給AQS提供條件變量的支持

ConditionObject隊(duì)列與CLH隊(duì)列的愛恨情仇:
調(diào)用了await()方法的線程,會(huì)被加入到conditionObject等待隊(duì)列中,并且喚醒CLH隊(duì)列中head節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)。
線程在某個(gè)ConditionObject對(duì)象上調(diào)用了singnal()方法后,等待隊(duì)列中的firstWaiter會(huì)被加入到AQS的CLH隊(duì)列中,等待被喚醒。
當(dāng)線程調(diào)用unLock()方法釋放鎖時(shí),CLH隊(duì)列中的head節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)(在本例中是firtWaiter),會(huì)被喚醒。
31.4 模板方法設(shè)計(jì)模式
模板方法模式:在一個(gè)方法中定義一個(gè)算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變算法結(jié)構(gòu)的情況下,重新定義算法中的某些步驟。
AQS的典型設(shè)計(jì)模式就是模板方法設(shè)計(jì)模式啦。AQS全家桶(ReentrantLock,Semaphore)的衍生實(shí)現(xiàn),就體現(xiàn)出這個(gè)設(shè)計(jì)模式。如AQS提供tryAcquire,tryAcquireShared等模板方法,給子類實(shí)現(xiàn)自定義的同步器。
31.5 獨(dú)占與共享模式。
獨(dú)占式: 同一時(shí)刻僅有一個(gè)線程持有同步狀態(tài),如ReentrantLock。又可分為公平鎖和非公平鎖。
共享模式:多個(gè)線程可同時(shí)執(zhí)行,如Semaphore/CountDownLatch等都是共享式的產(chǎn)物。
31.6 自定義同步器
你要實(shí)現(xiàn)自定義鎖的話,首先需要確定你要實(shí)現(xiàn)的是獨(dú)占鎖還是共享鎖,定義原子變量state的含義,再定義一個(gè)內(nèi)部類去繼承AQS,重寫對(duì)應(yīng)的模板方法即可啦
32 Semaphore原理
Semaphore,我們也把它叫做信號(hào)量。可以用來控制同時(shí)訪問特定資源的線程數(shù)量,通過協(xié)調(diào)各個(gè)線程,以保證合理的使用資源。
我們可以把它簡(jiǎn)單的理解成我們停車場(chǎng)入口立著的那個(gè)顯示屏,每當(dāng)有一輛車進(jìn)入停車場(chǎng)顯示屏就會(huì)顯示剩余車位減1,每有一輛車從停車場(chǎng)出去,顯示屏上顯示的剩余車輛就會(huì)加1,當(dāng)顯示屏上的剩余車位為0時(shí),停車場(chǎng)入口的欄桿就不會(huì)再打開,車輛就無法進(jìn)入停車場(chǎng)了,直到有一輛車從停車場(chǎng)出去為止。
32.1 Semaphore使用demo
我們就以停車場(chǎng)的例子,來實(shí)現(xiàn)demo。
假設(shè)停車場(chǎng)最多可以停20輛車,現(xiàn)在有100輛要進(jìn)入停車場(chǎng)。
我們很容易寫出以下代碼;
32.2 Semaphore原理
我們來看下實(shí)現(xiàn)的原理是怎樣的。
Semaphore構(gòu)造函數(shù)
可用令牌數(shù)
獲取令牌
釋放令牌
Semaphore構(gòu)造函數(shù)
它會(huì)創(chuàng)建一個(gè)非公平的鎖的同步阻塞隊(duì)列,并且把初始令牌數(shù)量(20)賦值給同步隊(duì)列的state,這個(gè)state就是AQS
的哈。
2.可用令牌數(shù)
這個(gè)availablePermits
,獲取的就是state
值。剛開始為20,所以肯定不會(huì)為0嘛。
3.獲取令牌
接著我們?cè)倏聪芦@取令牌的API
獲取1個(gè)令牌
嘗試獲取令牌,使用了CAS算法。
可獲取令牌的話,就創(chuàng)建節(jié)點(diǎn),加入阻塞隊(duì)列;重雙向鏈表的head,tail節(jié)點(diǎn)關(guān)系,清空無效節(jié)點(diǎn);掛起當(dāng)前節(jié)點(diǎn)線程
4.釋放令牌
33 synchronized做了哪些優(yōu)化?什么是偏向鎖?什么是自旋鎖?鎖租化?
在JDK1.6之前,synchronized的實(shí)現(xiàn)直接調(diào)用ObjectMonitor的enter和exit,這種鎖被稱之為重量級(jí)鎖。從JDK6開始,HotSpot虛擬機(jī)開發(fā)團(tuán)隊(duì)對(duì)Java中的鎖進(jìn)行優(yōu)化,如增加了適應(yīng)性自旋、鎖消除、鎖粗化、輕量級(jí)鎖和偏向鎖等優(yōu)化策略,提升了synchronized的性能。
偏向鎖:在無競(jìng)爭(zhēng)的情況下,只是在Mark Word里存儲(chǔ)當(dāng)前線程指針,CAS操作都不做。
輕量級(jí)鎖:在沒有多線程競(jìng)爭(zhēng)時(shí),相對(duì)重量級(jí)鎖,減少操作系統(tǒng)互斥量帶來的性能消耗。但是,如果存在鎖競(jìng)爭(zhēng),除了互斥量本身開銷,還額外有CAS操作的開銷。
自旋鎖:減少不必要的CPU上下文切換。在輕量級(jí)鎖升級(jí)為重量級(jí)鎖時(shí),就使用了自旋加鎖的方式
鎖粗化:將多個(gè)連續(xù)的加鎖、解鎖操作連接在一起,擴(kuò)展成一個(gè)范圍更大的鎖。
鎖消除:虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。
34 什么是上下文切換?
什么是CPU上下文?
CPU 寄存器,是CPU內(nèi)置的容量小、但速度極快的內(nèi)存。而程序計(jì)數(shù)器,則是用來存儲(chǔ) CPU 正在執(zhí)行的指令位置、或者即將執(zhí)行的下一條指令位置。它們都是 CPU 在運(yùn)行任何任務(wù)前,必須的依賴環(huán)境,因此叫做CPU上下文。
什么是CPU上下文切換?
它是指,先把前一個(gè)任務(wù)的CPU上下文(也就是CPU寄存器和程序計(jì)數(shù)器)保存起來,然后加載新任務(wù)的上下文到這些寄存器和程序計(jì)數(shù)器,最后再跳轉(zhuǎn)到程序計(jì)數(shù)器所指的新位置,運(yùn)行新任務(wù)。
一般我們說的上下文切換,就是指內(nèi)核(操作系統(tǒng)的核心)在CPU上對(duì)進(jìn)程或者線程進(jìn)行切換。進(jìn)程從用戶態(tài)到內(nèi)核態(tài)的轉(zhuǎn)變,需要通過系統(tǒng)調(diào)用來完成。系統(tǒng)調(diào)用的過程,會(huì)發(fā)生CPU上下文的切換。
所以大家有時(shí)候會(huì)聽到這種說法,線程的上下文切換。 它指,CPU資源的分配采用了時(shí)間片輪轉(zhuǎn),即給每個(gè)線程分配一個(gè)時(shí)間片,線程在時(shí)間片內(nèi)占用 CPU 執(zhí)行任務(wù)。當(dāng)線程使用完時(shí)間片后,就會(huì)處于就緒狀態(tài)并讓出 CPU 讓其他線程占用,這就是線程的上下文切換??磦€(gè)圖,可能會(huì)更容易理解一點(diǎn)

35.為什么wait(),notify(),notifyAll()在對(duì)象中,而不在Thread類中
鎖只是個(gè)一個(gè)標(biāo)記,存在對(duì)象頭里面。
下面從面向?qū)ο蠛陀^察者模式角度來分析。
面向?qū)ο蟮慕嵌龋何覀兛梢园褀ait和notify直接理解為get和set方法。wait和notify方法都是對(duì)對(duì)象的鎖進(jìn)行操作,那么自然這些方法應(yīng)該屬于對(duì)象。舉例來說,門對(duì)象上有鎖屬性,開鎖和關(guān)鎖的方法應(yīng)該屬于門對(duì)象,而不應(yīng)該屬于人對(duì)象。
從觀察者模式的角度:對(duì)象是被觀察者,線程是觀察者。被觀察者的狀態(tài)如果發(fā)生變化,理應(yīng)有被觀察者去輪詢通知觀察者,否則的話,觀察者怎么知道notify方法應(yīng)該在哪個(gè)時(shí)刻調(diào)用?n個(gè)觀察者的notify又如何做到同時(shí)調(diào)用
36. 線程池中 submit()和 execute()方法有什么區(qū)別?
execute和submit都屬于線程池的方法,execute只能提交Runnable類型的任務(wù),而submit既能提交Runnable類型任務(wù)也能提交Callable類型任務(wù)。
execute會(huì)直接拋出任務(wù)執(zhí)行時(shí)的異常,submit會(huì)吃掉異常,可通過Future的get方法將任務(wù)執(zhí)行時(shí)的異常重新拋出。
execute所屬頂層接口是Executor,submit所屬頂層接口是ExecutorService,實(shí)現(xiàn)類ThreadPoolExecutor重寫了execute方法,抽象類AbstractExecutorService重寫了submit方法。
37 AtomicInteger 的原理?
AtomicInteger的底層,是基于CAS實(shí)現(xiàn)的。我們可以看下AtomicInteger的添加方法。如下
注意:compareAndSwapInt
是一個(gè)native方法哈,它是基于CAS來操作int類型的變量。并且,其它的原子操作類基本也大同小異。
38 Java中用到的線程調(diào)度算法是什么?
我們知道有兩種調(diào)度模型:分時(shí)調(diào)度和搶占式調(diào)度。
分時(shí)調(diào)度模型:讓所有的線程輪流獲得cpu的使用權(quán),并且平均分配每個(gè)線程占用的 CPU 的時(shí)間片。
搶占式調(diào)度:優(yōu)先讓可運(yùn)行池中優(yōu)先級(jí)高的線程占用CPU,如果可運(yùn)行池中的線程優(yōu)先級(jí)相同,那么就隨機(jī)選擇一個(gè)線程,使其占用CPU。處于運(yùn)行狀態(tài)的線程會(huì)一直運(yùn)行,直至它不得不放棄 CPU。
Java默認(rèn)的線程調(diào)度算法是搶占式。即線程用完CPU之后,操作系統(tǒng)會(huì)根據(jù)線程優(yōu)先級(jí)、線程饑餓情況等數(shù)據(jù)算出一個(gè)總的優(yōu)先級(jí)并分配下一個(gè)時(shí)間片給某個(gè)線程執(zhí)行。
39. shutdown() 和 shutdownNow()的區(qū)別
shutdownNow()能立即停止線程池,正在跑的和正在等待的任務(wù)都停下了。這樣做立即生效,但是風(fēng)險(xiǎn)也比較大。
shutdown()只是關(guān)閉了提交通道,用submit()是無效的;而內(nèi)部的任務(wù)該怎么跑還是怎么跑,跑完再徹底停止線程池。
40 說說幾種常見的線程池及使用場(chǎng)景?
幾種常用線程池:
newFixedThreadPool (固定數(shù)目線程的線程池)
newCachedThreadPool(可緩存線程的線程池)
newSingleThreadExecutor(單線程的線程池)
newScheduledThreadPool(定時(shí)及周期執(zhí)行的線程池)
40.1 newFixedThreadPool
核心線程數(shù)和最大線程數(shù)大小一樣
沒有所謂的非空閑時(shí)間,即keepAliveTime為0
阻塞隊(duì)列為無界隊(duì)列LinkedBlockingQueue
使用場(chǎng)景
FixedThreadPool 適用于處理CPU密集型的任務(wù),確保CPU在長(zhǎng)期被工作線程使用的情況下,盡可能的少的分配線程,即適用執(zhí)行長(zhǎng)期的任務(wù)。
40.2 newCachedThreadPool
核心線程數(shù)為0
最大線程數(shù)為Integer.MAX_VALUE
阻塞隊(duì)列是SynchronousQueue
非核心線程空閑存活時(shí)間為60秒
使用場(chǎng)景
當(dāng)提交任務(wù)的速度大于處理任務(wù)的速度時(shí),每次提交一個(gè)任務(wù),就必然會(huì)創(chuàng)建一個(gè)線程。極端情況下會(huì)創(chuàng)建過多的線程,耗盡 CPU 和內(nèi)存資源。由于空閑 60 秒的線程會(huì)被終止,長(zhǎng)時(shí)間保持空閑的 CachedThreadPool 不會(huì)占用任何資源。
40.3 newSingleThreadExecutor 單線程的線程池
核心線程數(shù)為1
最大線程數(shù)也為1
阻塞隊(duì)列是LinkedBlockingQueue
keepAliveTime為0
使用場(chǎng)景
適用于串行執(zhí)行任務(wù)的場(chǎng)景,一個(gè)任務(wù)一個(gè)任務(wù)地執(zhí)行。
40.4 newScheduledThreadPool
最大線程數(shù)為Integer.MAX_VALUE
阻塞隊(duì)列是DelayedWorkQueue
keepAliveTime為0
scheduleAtFixedRate() :按某種速率周期執(zhí)行
scheduleWithFixedDelay():在某個(gè)延遲后執(zhí)行
使用場(chǎng)景
周期性執(zhí)行任務(wù)的場(chǎng)景,需要限制線程數(shù)量的場(chǎng)景
41 什么是FutureTask
FutureTask是一種可以取消的異步的計(jì)算任務(wù)。它的計(jì)算是通過Callable
實(shí)現(xiàn)的,可以把它理解為是可以返回結(jié)果的Runnable
。
使用FutureTask的優(yōu)點(diǎn):
可以獲取線程執(zhí)行后的返回結(jié)果;
提供了超時(shí)控制功能。
它實(shí)現(xiàn)了Runnable
接口和Future
接口,底層基于生產(chǎn)者消費(fèi)者模式實(shí)現(xiàn)。
FutureTask用于在異步操作場(chǎng)景中,F(xiàn)utureTask作為生產(chǎn)者(執(zhí)行FutureTask的線程)和消費(fèi)者(獲取FutureTask結(jié)果的線程)的橋梁,如果生產(chǎn)者先生產(chǎn)出了數(shù)據(jù),那么消費(fèi)者get時(shí)能會(huì)直接拿到結(jié)果;如果生產(chǎn)者還未產(chǎn)生數(shù)據(jù),那么get時(shí)會(huì)一直阻塞或者超時(shí)阻塞,一直到生產(chǎn)者產(chǎn)生數(shù)據(jù)喚醒阻塞的消費(fèi)者為止。
42 java中interrupt(),interrupted()和isInterrupted()的區(qū)別
interrupt
它是真正觸發(fā)中斷的方法。interrupted
是Thread中的一個(gè)類方法,它也調(diào)用了isInterrupted(true)方法,不過它傳遞的參數(shù)是true,表示將會(huì)清除中斷標(biāo)志位。isInterrupted
是Thread
類中的一個(gè)實(shí)例方法,可以判斷實(shí)例線程是否被中斷。。
43 ?有三個(gè)線程T1,T2,T3,怎么確保它們按順序執(zhí)行
可以使用join方法
解決這個(gè)問題。比如在線程A中,調(diào)用線程B的join方法表示的意思就是:A等待B線程執(zhí)行完畢后(釋放CPU執(zhí)行權(quán)),在繼續(xù)執(zhí)行。
代碼如下:
44 有哪些阻塞隊(duì)列
ArrayBlockingQueue ? ? ? 一個(gè)由數(shù)組構(gòu)成的有界阻塞隊(duì)列
LinkedBlockingQueue ? ? ?一個(gè)由鏈表構(gòu)成的有界阻塞隊(duì)列
PriorityBlockingQueue ? ?一個(gè)支持優(yōu)先級(jí)排序的無界阻塞隊(duì)列
DelayQueue ? ? ? ? ? ? ? 一個(gè)使用優(yōu)先隊(duì)列實(shí)現(xiàn)的無界阻塞隊(duì)列。
SynchroniouQueue ? ? ? ? 一個(gè)不儲(chǔ)存元素的阻塞隊(duì)列
LinkedTransferQueue ? ? ?一個(gè)由鏈表結(jié)構(gòu)組成的無界阻塞隊(duì)列
LinkedBlockingDeque ? ? ?一個(gè)由鏈表結(jié)構(gòu)組成的雙向阻塞隊(duì)列
45 Java中ConcurrentHashMap的并發(fā)度是什么?
并發(fā)度就是segment
的個(gè)數(shù),通常是2的N次方。默認(rèn)是16
46 Java線程有哪些常用的調(diào)度方法?

46.1 線程休眠
Thread.sleep(long)
方法,使線程轉(zhuǎn)到超時(shí)等待阻塞(TIMED_WAITING) 狀態(tài)。long
參數(shù)設(shè)定睡眠的時(shí)間,以毫秒為單位。當(dāng)睡眠結(jié)束后,線程自動(dòng)轉(zhuǎn)為就緒(Runnable)
狀態(tài)。
46.2 線程中斷
interrupt()
表示中斷線程。需要注意的是,InterruptedException
是線程自己從內(nèi)部拋出的,并不是interrupt()
方法拋出的。對(duì)某一線程調(diào)用interrupt()
時(shí),如果該線程正在執(zhí)行普通的代碼,那么該線程根本就不會(huì)拋出InterruptedException
。但是,一旦該線程進(jìn)入到wait()/sleep()/join()
后,就會(huì)立刻拋出InterruptedException??梢杂?code>isInterrupted()來獲取狀態(tài)。
46.3 線程等待
Object
類中的wait()
方法,會(huì)導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對(duì)象的notify()
方法或notifyAll()
喚醒方法。
46.4 線程讓步
Thread.yield()
方法,暫停當(dāng)前正在執(zhí)行的線程對(duì)象,把執(zhí)行機(jī)會(huì)讓給相同或者更高優(yōu)先級(jí)的線程。
46.5 線程通知
Object的notify()
方法,喚醒在此對(duì)象監(jiān)視器上等待的單個(gè)線程。如果所有線程都在此對(duì)象上等待,則會(huì)選擇喚醒其中一個(gè)線程。選擇是任意性的,并在對(duì)實(shí)現(xiàn)做出決定時(shí)發(fā)生。
notifyAll()
,則是喚醒在此對(duì)象監(jiān)視器上等待的所有線程。
47. ReentrantLock的加鎖原理
ReentrantLock
,是可重入鎖,是JDK5中添加在并發(fā)包下的一個(gè)高性能的工具。它支持同一個(gè)線程在未釋放鎖的情況下重復(fù)獲取鎖。
47.1 ReentrantLock使用的模板
我們先來看下是ReentrantLock使用的模板:
47.2 什么是非公平鎖,什么是公平鎖?
ReentrantLock無參構(gòu)造函數(shù),默認(rèn)創(chuàng)建的是非公平鎖,如下:
而通過fair參數(shù)指定使用公平鎖(FairSync)還是非公平鎖(NonfairSync)
什么是公平鎖?
公平鎖:多個(gè)線程按照申請(qǐng)鎖的順序去獲得鎖,線程會(huì)直接進(jìn)入隊(duì)列去排隊(duì),永遠(yuǎn)都是隊(duì)列的第一位才能得到鎖。
優(yōu)點(diǎn):所有的線程都能得到資源,不會(huì)餓死在隊(duì)列中。
缺點(diǎn):吞吐量會(huì)下降很多,隊(duì)列里面除了第一個(gè)線程,其他的線程都會(huì)阻塞,cpu喚醒阻塞線程的開銷會(huì)很大。
什么是非公平鎖?
非公平鎖:多個(gè)線程去獲取鎖的時(shí)候,會(huì)直接去嘗試獲取,獲取不到,再去進(jìn)入等待隊(duì)列,如果能獲取到,就直接獲取到鎖。
優(yōu)點(diǎn):可以減少CPU喚醒線程的開銷,整體的吞吐效率會(huì)高點(diǎn),CPU也不必取喚醒所有線程,會(huì)減少喚起線程的數(shù)量。
缺點(diǎn):你們可能也發(fā)現(xiàn)了,這樣可能導(dǎo)致隊(duì)列中間的線程一直獲取不到鎖或者長(zhǎng)時(shí)間獲取不到鎖,導(dǎo)致餓死。
47.3 lock()加鎖流程

大家可以結(jié)合AQS + 公平鎖/非公平鎖 + CAS去講ReentrantLock的原理哈。
48. 線程間的通訊方式

48.1 volatile和synchronized關(guān)鍵字
volatile關(guān)鍵字用來修飾共享變量,保證了共享變量的可見性,任何線程需要讀取時(shí)都要到內(nèi)存中讀?。ù_保獲得最新值)。
synchronized關(guān)鍵字確保只能同時(shí)有一個(gè)線程訪問方法或者變量,保證了線程訪問的可見性和排他性。
48.2 等待/通知機(jī)制
等待/通知機(jī)制,是指一個(gè)線程A調(diào)用了對(duì)象O的wait()方法進(jìn)入等待狀態(tài),而另一個(gè)線程B 調(diào)用了對(duì)象O的notify()或者notifyAll()方法,線程A收到通知后從對(duì)象O的wait()方法返回,進(jìn)而 執(zhí)行后續(xù)操作。
48.3 管道輸入/輸出流
管道輸入/輸出流和普通的文件輸入/輸出流或者網(wǎng)絡(luò)輸入/輸出流不同之處在于,它主要 用于線程之間的數(shù)據(jù)傳輸,而傳輸?shù)拿浇闉閮?nèi)存。
管道輸入/輸出流主要包括了如下4種具體實(shí)現(xiàn):PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前兩種面向字節(jié),而后兩種面向字符。
48.4 join()方法
如果一個(gè)線程A執(zhí)行了thread.join()語句,其含義是:當(dāng)前線程A等待thread線程終止之后才 從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個(gè)具備超時(shí)特性的方法。這兩個(gè)超時(shí)方法表示,如果線程thread在給定的超時(shí) 時(shí)間里沒有終止,那么將會(huì)從該超時(shí)方法中返回。
48.5 ThreadLocal
ThreadLocal,即線程本地變量(每個(gè)線程都有自己唯一的一個(gè)哦),是一個(gè)以ThreadLocal對(duì)象為鍵、任意對(duì)象為值的存儲(chǔ)結(jié)構(gòu)。底層是一個(gè)ThreadLocalMap來存儲(chǔ)信息,key是弱引用,value是強(qiáng)引用,所以使用完畢后要及時(shí)清理(尤其使用線程池時(shí))。
49 ?寫出3條你遵循的多線程最佳實(shí)踐
多用同步類,少用wait,notify
少用鎖,應(yīng)當(dāng)縮小同步范圍
給線程一個(gè)自己的名字
多用并發(fā)集合少用同步集合
50. 為什么阿里發(fā)布的 Java開發(fā)手冊(cè)中強(qiáng)制線程池不允許使用 Executors 去創(chuàng)建?
這是因?yàn)?,JDK開發(fā)者提供了線程池的實(shí)現(xiàn)類都是有坑的,如newFixedThreadPool
和newCachedThreadPool
都有內(nèi)存泄漏的坑。
最后
??? 小伙伴們學(xué)習(xí)編程,有時(shí)候不知道怎么學(xué),從哪里開始學(xué)。掌握了基本的一些語法或者做了兩個(gè)案例后,不知道下一步怎么走,不知道如何去學(xué)習(xí)更加高深的知識(shí)。
那么對(duì)于這些小伙伴們,我準(zhǔn)備了大量的視頻教程,PDF電子書籍,以及源代碼!
只要+up主威信wangkeit1備注“B站”就可以白嫖領(lǐng)取啦!
