## 29 押寶線程源碼面試題
## 引導語
關于線程方面的面試題,大部分都是概念題,我們需要大概的清楚這些概念,和面試官達成共識即可,本章我們一起來看下這些面試題,對前兩章的學習進行鞏固。
### 1 面試題
#### 1.1 創建子線程時,子線程是得不到父線程的 ThreadLocal,有什么辦法可以解決這個問題?
答:這道題主要考察線程的屬性和創建過程,可以這么回答。
可以使用 InheritableThreadLocal 來代替 ThreadLocal,ThreadLocal 和 InheritableThreadLocal 都是線程的屬性,所以可以做到線程之間的數據隔離,在多線程環境下我們經常使用,但在有子線程被創建的情況下,父線程 ThreadLocal 是無法傳遞給子線程的,但 InheritableThreadLocal 可以,主要是因為在線程創建的過程中,會把
InheritableThreadLocal 里面的所有值傳遞給子線程,具體代碼如下:
```
// 當父線程的 inheritableThreadLocals 的值不為空時 // 會把 inheritableThreadLocals 里面的值全部傳遞給子線程 if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
```
#### 1.2 線程創建有幾種實現方式?
答:主要有三種,分成兩大類,第一類是子線程沒有返回值,第二類是子線程有返回值。
無返回值的線程有兩種寫法,第一種是繼承 Thread,可以這么寫:
```
class MyThread extends Thread{ @Override public void run() { log.info(Thread.currentThread().getName()); } } @Test public void extendThreadInit(){ new MyThread().start(); }
```
第二種是實現 Runnable 接口,并作為 Thread 構造器的入參,代碼如下:
```
Thread thread = new Thread(new Runnable() { @Override public void run() { log.info("{} begin run",Thread.currentThread().getName()); } }); // 開一個子線程去執行 thread.start();
```
這兩種都會開一個子線程去執行任務,并且是沒有返回值的,如果需要子線程有返回值,需要使用 Callable 接口,但 Callable 接口是無法直接作為 Thread 構造器的入參的,必須結合 FutureTask 一起使用,可以這樣寫代碼:
```
@Test public void testThreadByCallable() throws ExecutionException, InterruptedException { FutureTask futureTask = new FutureTask(new Callable<String> () { @Override public String call() throws Exception { Thread.sleep(3000); String result = "我是子線程"+Thread.currentThread().getName(); log.info("子線程正在運行:{}",Thread.currentThread().getName()); return result; } }); new Thread(futureTask).start(); log.info("返回的結果是 {}",futureTask.get()); }
```
把 FutureTask 作為 Thread 的入參就可以了,FutureTask 組合了 Callable ,使我們可以使用 Callable,并且 FutureTask 實現了 Runnable 接口,使其可以作為 Thread 構造器的入參,還有 FutureTask 實現了 Future,使其對任務有一定的管理功能。
#### 1.3 子線程 1 去等待子線程 2 執行完成之后才能執行,如何去實現?
答:這里考察的就是 Thread.join 方法,我們可以這么做:
```
@Test public void testJoin2() throws Exception { Thread thread2 = new Thread(new Runnable() { @Override
public void run() { log.info("我是子線程 2,開始沉睡"); try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } log.info("我是子線程 2,執行完成"); } }); Thread thread1 = new Thread(new Runnable() { @Override public void run() { log.info("我是子線程 1,開始運行"); try { log.info("我是子線程 1,我在等待子線程 2"); // 這里是代碼關鍵 thread2.join(); log.info("我是子線程 1,子線程 2 執行完成,我繼續執行"); } catch (InterruptedException e) { e.printStackTrace(); } log.info("我是子線程 1,執行完成"); } }); thread1.start(); thread2.start(); Thread.sleep(100000); }
```
子線程 1 需要等待子線程 2,只需要子線程 1 運行的時候,調用子線程 2 的 join 方法即可,這樣線程 1 執行到 join 代碼時,就會等待線程 2 執行完成之后,才會繼續執行。
#### 1.4 守護線程和非守護線程的區別?如果我想在項目啟動的時候收集代碼信息,請問是守護線程好,還是非守護線程好,為什么?
答:兩者的主要區別是,在 JVM 退出時,JVM 是不會管守護線程的,只會管非守護線程,如果非守護線程還有在運行的,JVM 就不會退出,如果沒有非守護線程了,但還有守護線程的,JVM 直接退出。
如果需要在項目啟動的時候收集代碼信息,就需要看收集工作是否重要了,如果不太重要,又很耗時,就應該選擇守護線程,這樣不會妨礙 JVM 的退出,如果收集工作非常重要的話,那么就需要非守護進程,這樣即使啟動時發生未知異常,JVM 也會等到代碼收集信息線程結束后才會退出,不會影響收集工作。
#### 1.5 線程 start 和 run 之間的區別。
答:調用 Thread.start 方法會開一個新的線程,run 方法不會。
#### 1.6 Thread、Runnable、Callable 三者之間的區別。
答:Thread 實現了 Runnable,本身就是 Runnable,但同時負責線程創建、線程狀態變更等操作。
Runnable 是無返回值任務接口,Callable 是有返回值任務接口,如果任務需要跑起來,必須需要 Thread 的支持才行,Runnable 和 Callable 只是任務的定義,具體執行還需要靠 Thread。
#### 1.7 線程池 submit 有兩個方法,方法一可接受 Runnable,方法二可接受 Callable,但兩個方法底層的邏輯卻是同一套,這是如何適配的。
答:問題考察點在于 Runnable 和 Callable 之間是如何轉化的,可以這么回答。
Runnable 和 Callable 是通過 FutureTask 進行統一的,FutureTask 有個屬性是 Callable,同時也實現了 Runnable 接口,兩者的統一轉化是在 FutureTask 的構造器里實現的,FutureTask 的最終目標是把 Runnable 和 Callable 都轉化成 Callable,Runnable 轉化成 Callable 是通過 RunnableAdapter 適配器進行實現的。
線程池的 submit 底層的邏輯只認 FutureTask,不認 Runnable 和 Callable 的差異,所以只要都轉化成 FutureTask,底層實現都會是同一套。
具體 Runnable 轉化成 Callable 的代碼和邏輯可以參考上一章,有非常詳細的描述。
#### 1.8 Callable 能否丟給 Thread 去執行?
答:可以的,可以新建 Callable,并作為 FutureTask 的構造器入參,然后把 FutureTask 丟給 Thread 去執行即可。
#### 1.9 FutureTask 有什么作用(談談對 FutureTask 的理解)。
答:作用如下:
1. 組合了 Callable,實現了 Runnable,把 Callable 和 Runnnable 串聯了起來。
2. 統一了有參任務和無參任務兩種定義方式,方便了使用。
3. 實現了 Future 的所有方法,對任務有一定的管理功能,比如說拿到任務執行結果,取消任務,打斷任務等等。
#### 1.10 聊聊對 FutureTask 的 get、cancel 方法的理解
答:get 方法主要作用是得到 Callable 異步任務執行的結果,無參 get 會一直等待任務執行完成之后才返回,有參 get 方法可以設定固定的時間,在設定的時間內,如果任務還沒有執行成功,
直接返回異常,在實際工作中,建議多多使用
get 有參方法,少用 get 無參方法,防止任務執行過慢時,多數線程都在等待,造成線程耗盡的問題。
cancel 方法主要用來取消任務,如果任務還沒有執行,是可以取消的,如果任務已經在執行過程中了,你可以選擇不取消,或者直接打斷執行中的任務。
兩個方法具體的執行步驟和原理見上一章節源碼解析。
#### 1.11 Thread.yield 方法在工作中有什么用?
答:yield 方法表示當前線程放棄 cpu,重新參與到 cpu 的競爭中去,再次競爭時,自己有可能得到 cpu 資源,也有可能得不到,這樣做的好處是防止當前線程一直霸占 cpu。
我們在工作中可能會寫一些 while 自旋的代碼,如果我們一直 while 自旋,不采取任何手段,我們會發現 cpu 一直被當前 while 循環占用,如果能預見 while 自旋時間很長,我們會設置一定的判斷條件,讓當前線程陷入阻塞,如果能預見 while 自旋時間很短,我們通常會使用 Thread.yield 方法,使當前自旋線程讓步,不一直霸占 cpu,比如這樣:
```
boolean stop = false; while (!stop){ // dosomething Thread.yield(); }
```
#### 1.12 wait()和sleep()的相同點和區別?
答:
相同點:
1. 兩者都讓線程進入到 TIMED_WAITING 狀態,并且可以設置等待的時間。
不同點:
1. wait 是 Object 類的方法,sleep 是 Thread 類的方法。
2. sleep 不會釋放鎖,沉睡的時候,其它線程是無法獲得鎖的,但 wait 會釋放鎖。
#### 1.13 寫一個簡單的死鎖 demo
```
// 共享變量 1 private static final Object share1 = new Object(); // 共享變量 2 private static final Object share2 = new Object(); @Test public void testDeadLock() throws InterruptedException { // 初始化線程 1,線程 1 需要在鎖定 share1 共享資源的情況下再鎖定 share2 Thread thread1 = new Thread(() -> { synchronized (share1){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (share2){ log.info("{} is run",Thread.currentThread().getName()); } } }); // 初始化線程 2,線程 2 需要在鎖定 share2 共享資源的情況下再鎖定 share1 Thread thread2 = new Thread(() -> { synchronized (share2){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace();
} synchronized (share1){ log.info("{} is run",Thread.currentThread().getName()); } } }); // 當線程 1、2 啟動后,都在等待對方鎖定的資源,但都得不到,造成死鎖 thread1.start(); thread2.start(); Thread.sleep(1000000000); }
```
### 2 總結
線程章節算是中等難度,我們需要清楚線程的概念,線程如何初始化,線程的狀態變更等等問題,這些知識點都是線程池、鎖的基礎,學好線程后,再學習線程池和鎖就會輕松很多。
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題