面试必会系列 - 1.3 Java 多线程
本文已收錄至 github,完整圖文:https://github.com/HanquanHq/MD-Notes
多線程
線程有多少種狀態(tài)?
指定時(shí)刻,線程只可能處于下面 6 種不同狀態(tài) 的其中一個(gè)狀態(tài)(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié))
線程在生命周期中,而是隨著代碼的執(zhí)行在不同狀態(tài)之間切換。Java 線程狀態(tài)變遷如下圖所示(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié)):
原圖中 wait 到 runnable 狀態(tài)的轉(zhuǎn)換中,join實(shí)際上是Thread類的方法,但這里寫成了Object。
在代碼中,可以使用 getState 獲取線程狀態(tài)
Thread t = new MyThread(); System.out.println(t.getState());面試題:既然調(diào)用 start() 方法時(shí)會(huì)執(zhí)行 run() 方法,為什么不能直接調(diào)用 run() 方法?
new 一個(gè) Thread,線程進(jìn)入了新建狀態(tài);調(diào)用 start() 方法,會(huì)啟動(dòng)一個(gè)線程并使線程進(jìn)入了就緒狀態(tài),當(dāng)分配到時(shí)間片后就可以開始運(yùn)行了。 start() 會(huì)執(zhí)行線程的相應(yīng)準(zhǔn)備工作,然后自動(dòng)執(zhí)行 run() 方法的內(nèi)容,這是真正的多線程工作。
而直接執(zhí)行 run() 方法,會(huì)把 run 方法當(dāng)成一個(gè) main 線程下的普通方法去執(zhí)行,并不會(huì)在某個(gè)線程中執(zhí)行它,所以這并不是多線程工作。
總結(jié): 調(diào)用 start 方法方可啟動(dòng)線程并使線程進(jìn)入就緒狀態(tài),而 run 方法只是 thread 的一個(gè)普通方法調(diào)用,還是在主線程里執(zhí)行。
面試題:說(shuō)說(shuō) sleep() 方法和 wait() 方法區(qū)別和共同點(diǎn)?
-
兩者最主要的區(qū)別在于:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。
-
兩者都可以暫停線程的執(zhí)行。
-
wait 通常被用于線程間交互/通信,sleep 通常被用于暫停執(zhí)行。
-
wait() 方法被調(diào)用后,線程不會(huì)自動(dòng)蘇醒,需要?jiǎng)e的線程調(diào)用同一個(gè)對(duì)象上的 notify() 或者 notifyAll() 方法。或者可以使用 wait(long timeout)超時(shí)后線程會(huì)自動(dòng)蘇醒。
sleep() 方法執(zhí)行完成后,線程會(huì)自動(dòng)蘇醒。
多線程的使用
啟動(dòng)線程的三種方式
- 繼承 Thread 類,重寫 run 方法
- 實(shí)現(xiàn) Runnable 接口,重寫 run 方法(或Lambda表達(dá)式)
- 通過(guò)線程池來(lái)啟動(dòng) Executors.newCachedThread(實(shí)際上是以上兩種之一)
sleep yield join 的含義
- sleep:當(dāng)前線程暫停一段時(shí)間,讓別的線程去執(zhí)行。不釋放鎖。睡眠時(shí)間到,自動(dòng)復(fù)活
- yield:當(dāng)前線程執(zhí)行時(shí),停下來(lái)進(jìn)入等待隊(duì)列。系統(tǒng)調(diào)度算法決定哪個(gè)線程繼續(xù)運(yùn)行(有可能還是自己)
- join:在當(dāng)前線程加入調(diào)用的 join 線程,等調(diào)用的線程運(yùn)行完了,自己再繼續(xù)執(zhí)行
wait notify notifyAll 的含義
Object 對(duì)象中有三個(gè)方法 wait()、notify()、notifyAll(),它們的用途都是用來(lái)控制線程的狀態(tài)。
- wait:必須在同步方法/同步代碼塊中被調(diào)用;釋放鎖,調(diào)用該線程的方法進(jìn)入等待隊(duì)列,直到被喚醒
- notify:隨機(jī)喚醒等待隊(duì)列中等待的一個(gè)線程,使得該線程由 等待狀態(tài) 進(jìn)入 可運(yùn)行狀態(tài)
- notifyAll:喚醒在此對(duì)象監(jiān)視器上等待的所有線程,被喚醒的線程將以常規(guī)方式與在該對(duì)象上主動(dòng)同步的其他所有線程進(jìn)行競(jìng)爭(zhēng)
如何關(guān)閉線程?interrupt 的含義?
Java沒有提供任何機(jī)制來(lái)安全地終止線程。它提供了 interrupt,這僅僅是會(huì)通知到被終止的線程“你該停止運(yùn)行了”,由編寫者決定如何處理 InterruptedException
@Override public void run() {while (true && !Thread.currentThread().isInterrupted()) { // 判斷是否處于中斷狀態(tài)System.out.println("go");try {throwInMethod();} catch (InterruptedException e) {// 恢復(fù)設(shè)置中斷狀態(tài),便于在下一個(gè)循環(huán)的時(shí)候檢測(cè)到中斷狀態(tài),正常退出Thread.currentThread().interrupt();e.printStackTrace();// 可以記錄一下日志}} } // JDK鎖、框架源碼的一些實(shí)現(xiàn)使用過(guò)interrupt,實(shí)際工程中,很少有人用它控制業(yè)務(wù)邏輯不要使用 stop() 關(guān)閉線程,你應(yīng)該讓線程正常結(jié)束。
sleep阻塞時(shí)候會(huì)自動(dòng)檢測(cè)中斷:拋出異常 java.lang.InterruptedException: sleep interrupted
ThreadLocal
ThreadLocal 是線程引用對(duì)象,線程之間不共享。
-
為什么要有 ThreadLocal?
-
Spring的聲明式事務(wù)會(huì)用到。(Spring 的聲明式事務(wù)在一個(gè)線程里)
-
connection 在連接池里,不同的 connection 之間怎么形成完整的事務(wù)?
把 connection 放在當(dāng)前線程的 ThreadLocal 里面,以后拿的時(shí)候從 ThreadLocal 直接拿,不去線池里面拿。
-
-
ThreadLocal是怎么做到線程獨(dú)有的?
- ThreadLocalMap是當(dāng)前 Thread 的一個(gè)成員變量,其 Key 是 ThreadLocal 對(duì)象,值是 Entry 對(duì)象,Entry 中只有一個(gè) Object 類的 vaule 值。
- 使用 虛引用,讓 Key 指向 ThreadLocal
強(qiáng)軟弱虛四種引用
-
強(qiáng)引用 StrongReference
- Object o = new Object()
- 只要有引用指向它,就算是OOM了,也不會(huì)被回收
-
軟引用 SoftReference
SoftReference<byte[]> m = new SoftReference<>(new byte[1024 * 1024 * 10]); System.out.println(m.get());- 內(nèi)存空間不夠時(shí),弱引用會(huì)被回收
- 用來(lái)做緩存
-
弱引用 WeakReference
WeakReference<M> m = new WeakReference<>(new M()); System.out.println(m.get());- 只要發(fā)生GC,弱引用就會(huì)被回收
- ThreadLocal 中會(huì)使用弱引用
-
虛引用 PhantomReference
虛引用,顧名思義,就是形同虛設(shè),與其他幾種引用都不同,虛引用并不會(huì)決定對(duì)象的生命周期。如果一個(gè)對(duì)象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時(shí)候都可能被垃圾回收。
private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>(); public static void main(String[] args) {PhantomReference<M> phantomReference = new PhantomReference<>(new M(), QUEUE);- 虛引用主要用來(lái)跟蹤對(duì)象被垃圾回收的活動(dòng),必須和 引用隊(duì)列 ReferenceQueue 聯(lián)合使用
- 虛引用對(duì)象被回收前,會(huì)被加入到隊(duì)列中,我們需要另一線程不斷檢測(cè)隊(duì)列,并以在所引用的對(duì)象的內(nèi)存被回收之前采取必要的行動(dòng)。
- 用來(lái)管理 堆外內(nèi)存,堆外內(nèi)存包括:
- 方法區(qū)
- NIO 的 DirectByteBuffer
線程池
線程池前值知識(shí)
Executor 接口關(guān)系
Callable
? 類似于Runnable,但是可以有返回值
Future
? 存儲(chǔ)將來(lái)執(zhí)行的結(jié)果。Callable被執(zhí)行完之后的結(jié)果,被封裝到Future里面。
FutureTask
? 更加靈活,是Runnable和Future的結(jié)合,既是一個(gè)Runnable,又可以存結(jié)果。
CompletableFuture
? 可以用來(lái)管理多個(gè)Future的結(jié)果,對(duì)各種各樣的結(jié)果進(jìn)行組合處理。提供了非常好用的接口,十分友好。
場(chǎng)景:假設(shè)你需要提供一個(gè)服務(wù),這個(gè)服務(wù)查詢 京東、淘寶、天貓 對(duì)于同一類產(chǎn)品的價(jià)格并匯總展示,你用CompletableFuture開啟三個(gè)線程,來(lái)完成這個(gè)任務(wù),三個(gè)任務(wù)全部完成之后,才能繼續(xù)向下運(yùn)行。
線程池
ThreadPoolExecutor:是我們通常所說(shuō)的線程池。多個(gè)線程共享同一個(gè)任務(wù)隊(duì)列。
-
SingleThreadPool
為什么會(huì)有單線程的線程池?單線程的線程池是有任務(wù)隊(duì)列的;線程池能幫你提供線程生命周期的管理。
- 線程池里面只有一個(gè)線程
- 保證我們?nèi)舆M(jìn)去的任務(wù)是被順序執(zhí)行的
-
CachedThreadPool
當(dāng)任務(wù)到來(lái)時(shí),如果有線程空閑,我就用現(xiàn)有的線程;如果所有線程忙,就啟動(dòng)一個(gè)新線程。
- 核心線程數(shù)為0
- 最大線程數(shù)是Integer.MAX_VALUE
- 保證任務(wù)不會(huì)堆積
- SynchronousQueue 是容量為0的阻塞隊(duì)列,每個(gè)插入操作必須等待另一個(gè)線程執(zhí)行相應(yīng)的刪除操作
-
FixedThreadPool
- 固定線程數(shù)的線程池
- 適合做一些并行的計(jì)算,比如你要找1-200000之內(nèi)所有的質(zhì)數(shù),你將這個(gè)大任務(wù)拆成4個(gè)小線程,共同去運(yùn)行,肯定比串行計(jì)算要更快。
-
ScheduledPool
- 專門用來(lái)執(zhí)行定時(shí)任務(wù)的一個(gè)線程池
ForkJoinPoll:先將任務(wù)分解,最后再匯總,可以有返回值或無(wú)返回值。每個(gè)線程有自己的任務(wù)隊(duì)列。
- WorkStealingPool
- 普通的線程池是有一個(gè)線程的集合,所有線程去同一個(gè)任務(wù)隊(duì)列里面取任務(wù),取出任務(wù)之后執(zhí)行,而 WorkStealingPool 是每一個(gè)線程都有自己獨(dú)立的任務(wù)隊(duì)列,如果某一個(gè)線程執(zhí)行完自己的任務(wù)之后,要去別的線程那里偷任務(wù),分擔(dān)別的線程的任務(wù)。
- WorkStealingPool 本質(zhì)上還是一個(gè) ForkJoinPool
自定義一個(gè)線程池
public class TestThreadPool {static class Task implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " is running task");}}public static void main(String[] args) {// ThreadPoolExecutor 7個(gè)參數(shù)ThreadPoolExecutor tpe = new ThreadPoolExecutor(2, 4,60, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(4),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy()); // 調(diào)用者處理服務(wù),這里是main調(diào)用for (int i = 0; i < 8; i++) { // 開啟8個(gè)任務(wù),放進(jìn)線程池執(zhí)行tpe.execute(new Task());}tpe.shutdown();} }new ThreadPoolExecutor() 7個(gè)參數(shù)
-
corePoolSize:核心線程數(shù)
-
maximumPoolSize:最大線程數(shù)
-
keepAliveTime:空閑線程生存時(shí)間
-
TimeUnit:生存時(shí)間的單位
-
BlockingQueue:各種各樣的任務(wù)隊(duì)列
-
ThreadFactory:線程工廠
可以使用Executors.defaultThreadFactory(),也可以自定義工廠,指定線程名稱
-
RejectedExecutionHandler:線程池忙且任務(wù)隊(duì)列滿時(shí)的 拒絕策略
- CallerRunsPolicy,讓調(diào)用者線程去處理任務(wù)
- AbortPolicy,拋異常
- DiscardPolicy,扔掉,不拋異常
- DiscardOldestPolicy,扔掉排隊(duì)時(shí)間最久的
總結(jié)
以上是生活随笔為你收集整理的面试必会系列 - 1.3 Java 多线程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 天勤数据结构:前缀、中缀、后缀表达式的转
- 下一篇: 面试必会系列 - 1.5 Java 锁机