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