大家好,我是王有志,歡迎來到《Java面試都問啥?》的第一篇技術文章。 這個系列會從Java部分開始,接著是MySQL和Redis的內容,同時會繼續更新數據結構與演算法的部分,這樣在第一階段,我們就完成了面試“三幻神”的挑戰。 Java的部分從併發編程開始,接著是Java虛擬機,最後是集合框架。至於J ...
大家好,我是王有志,歡迎來到《Java面試都問啥?》的第一篇技術文章。
這個系列會從Java部分開始,接著是MySQL和Redis的內容,同時會繼續更新數據結構與演算法的部分,這樣在第一階段,我們就完成了面試“三幻神”的挑戰。
Java的部分從併發編程開始,接著是Java虛擬機,最後是集合框架。至於Java基礎,因為大部分只是API的使用,所以只提供整理好的題目,而涉及到反射,動態代理等內容,會在集合框架完成後補充。
那麼話不多說,我們直接開始吧。
併發編程都問啥?
每個模塊開始時,我都會放出這一模塊中知識點的統計數據,供大家參考。
統計中,我將併發編程分為了5個知識點:
- 線程基礎:線程的基本概念,Thread類的使用等;
- 線程池:線程池的原理,線程池的使用等;
- synchronized:原理,鎖升級,優化等;
- volatile:原理,指令重排,JMM相關等;
- ThreadLocal:原理,使用方法,記憶體泄漏等;
- JUC:Lock介面,併發容器,CAS,AQS等。
統計到併發編程關鍵詞174次,線程出現37次,線程池出現22次,synchronized出現30次,volatile出現12次,ThreadLocal出現8次,JUC出現44次,剩餘21次僅提到多線程/併發編程。
從圖中看,ThreadLocal和volatile出現概率較低,但個人建議面試準備中,併發編程的部分要全量準備。
數據大家都看到了,接下來看看各大公司都會問哪些關於線程的問題。這部分題目主要收集自某準網面經,淺紫色底色的題目是我和小伙伴在面試過程遇到過的。
MarkDown的表格實在太醜了,偷個懶使用圖片代替了,文末附上整理後Excel的獲取方式。
關於線程你必須知道的8個問題
涉及到概念性的題目就不過多贅述了,這些可以通過百度百科獲取到答案。在這裡我挑選了8道比較有代表性的問題,和大家分享我的理解。
併發編程的3要素
併發編程的3要素:
- 原子性:操作不可分割,要麼不間斷的全部執行,要麼全部不執行;
- 有序性:指程式按照代碼的順序結構執行;
- 可見性:當一個線程修改了共用變數後,其它線程也是立即可見的。
概念很簡單,我們寫一些代碼展示下有序性和可見性的問題(原子性實在沒有想到很好的例子,有沒有小伙伴提供示例呢)。
有序性問題
public static class Singleton {
private Singleton instance;
public Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton() {
}
}
這是有序性問題的經典案例--未做同步控制的單例模式。當instance
還未初始化時,多個線程同時調用getInstance
方法,很容易出現其中一個線程獲取到的instance
為NULL。
這裡涉及Java創建對象的操作,CPU時間片分配的問題,解決它的辦法也有很多,暫時按下不表,放到volatile
關鍵字的內容中詳細解釋。
可見性問題
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
System.out.println("線程:" + Thread.currentThread().getName() + ",flag:" + flag);
}, "block_thread").start();
TimeUnit.MICROSECONDS.sleep(500);
new Thread(() -> {
flag = false;
System.out.println("線程:" + Thread.currentThread().getName() + ",flag:" + flag);
}, "change_thread").start();
}
很明顯,在change_thread中修改了flag,並不會使block_thread得到解脫,這就是共用變數線上程間不可見的問題。
Java創建線程的方式
通常網上的資料會給出4種創建線程的方式:
- 繼承Thread類
- 實現Runnable介面
- 實現Callable介面
- 通過線程池創建
先不評價這個答案的正確性,我們先來看看繼承Thread
類,實現Runnable
介面和實現Callable
介面是如何使用的。
繼承Thread類
public class ByThread {
public static void main(String[] args) throws InterruptedException {
System.out.println("main的線程:" + Thread.currentThread().getName());
MyThread myThread = new MyThread();
myThread.start();
}
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread的線程:" + Thread.currentThread().getName());
}
}
}
繼承Thread
類要實現run
方法,用於完成業務邏輯,該方法來自於Runnable
介面。啟動線程通過Thread.start
方法,方法內通過調用native方法start0
來啟動線程。
實現Runnable介面
public class ByRunnable {
public static void main(String[] args) {
System.out.println("main的線程:" + Thread.currentThread().getName());
new Thread(new MyRunnable()).start();
}
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable的線程:" + Thread.currentThread().getName());
}
}
}
實現Runnable
介面同樣要實現run
方法,啟動線程依舊是通過Thread.start
方法。
實質上繼承Thread
類和實現Runnable
介面沒有差別,只不過是隔代實現run
方法還是直接實現run
方法。
實現Callable介面
public class ByCallable {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main的線程:" + Thread.currentThread().getName());
Callable<String> callable = new MyCallable();
FutureTask <String> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println("MyCallable的執行線程:" + futureTask.get());
}
static class MyCallable implements Callable<String> {
@Override
public String call() {
System.out.println("MyCallable的線程:" + Thread.currentThread().getName());
return Thread.currentThread().getName();
}
}
}
實現Callable
介面看起來會複雜一些,但通過代碼可以看出來,最終還是回歸到Thread.start
方法,根據經驗,這種方式是不是和Runnable
有關係?
另外,我們註意到這種方式中藉助到了FutureTask
類,來看看FutureTask
的繼承關係:
不出所料,FutureTask
同樣要實現Runnable.run
方法,只不過這次由FutureTask
實現,FutureTask
在run
方法中調用Callable.call
方法來執行業務邏輯。
我們來回顧下這3種方式的特點,啟動線程都是通過Thread.start
方法,start
方法的基本執行單位是Runnable
介面,它們直接的差異在於如何實現Runnable.run
方法。另一個差異就是Callable.call
方法是有返回值的,而Runnable.run
方法沒有返回值。
使用線程池
public class ByThreadPool {
public static void main(String[] args) {
System.out.println("main的線程:" + Thread.currentThread().getName());
ExecutorService executorService = Executors.newSingleThreadExecutor();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("線程池的線程:" + Thread.currentThread().getName());
}
};
executorService.execute(runnable);
executorService.shutdown();
}
}
使用線程池依舊離不開Runnable.run
方法,會不會和Callable
一樣本質上還是Thread.start
?
如果不熟悉ThreadPoolExecutor
源碼的話,可以採用斷點的方式去跟蹤源碼,重點關註ThreadPoolExecutor.execute
和ThreadPoolExecutor.addWorker
兩個方法。
我們可以在addWorker
方法中發現兩行關鍵代碼:
final Thread t = w.thread;
t.start();
這證實了關於ThreadPoolExecutor
底層調用的猜想,最終依舊是通過Thread.start
方法啟動。
回到最初的問題,Java有幾種創建線程的方式?
如果從Java的層面來看,可以認為創建Thread
類的實例對象就完成了線程的創建,而調用Thread.start0
可以認為是操作系統層面的線程創建和啟動。
至於網上說的4種創建線程的方式,個人認為將它們歸類到線程中業務邏輯的實現方式更合理。
Java的線程狀態
Java中定義了6種線程狀態(與OS的線程狀態有差別),線程狀態的枚舉類被定義為Thread
的內部類State
。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
需要註意,Java中並未定義線程的RUNNING狀態,而是通過RUNNABLE包含了RUNNABLE(可運行)和RUNNING(運行中)。
建議大家閱讀源碼中的註釋,很清晰的解釋了每個狀態的場景。下麵我還是通過幾段代碼展示線程的不同狀態。
Tips:代碼中出現的TimeUnit.MILLISECONDS.sleep
是為了確保線程已經進入期望的狀態,如果不能很好的理解,文末附有Gitee地址,工程中的代碼有註釋。
常規狀態的轉換
這裡指的是線程從創建後(NEW),到啟動後(RUNNABLE),再到最後終止(TERMINATED)的一種無競爭的線程狀態轉換。
- NEW(新建):創建線程後尚未啟動(未調用
start
方法); - RUNNABLE(可運行):可運行狀態的線程在Java虛擬機中等待調度線程選中獲取CPU時間片;
- TERMINATED(終止):線程執行結束。
寫一段簡單的代碼來看下:
public class NormalStateTransition {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println("線程[" + thread.getName() + "]創建,狀態:[" + thread.getState() + "]");
thread.start();
System.out.println("線程[" + thread.getName() + "]啟動,狀態:[" + thread.getState() + "]");
TimeUnit.SECONDS.sleep(2);
System.out.println("線程[" + thread.getName() + "]結束,狀態:[" + thread.getState() + "]");
}
}
代碼非常簡單,這裡就不再解釋了。
常規狀態的轉換:
阻塞狀態的轉換
阻塞狀態是一種“異常”的狀態,通常是在等待資源。
BLOCKED(阻塞):等待監視器鎖而阻塞的線程狀態,處於阻塞狀態的線程正在等待監視器鎖進入同步的代碼塊/方法,或者在調用Object.wait
之後重新進入同步的代碼塊/方法。
再寫一段代碼:
public class BlockedStateTransition {
public static void main(String[] args) throws InterruptedException {
AtomicBoolean locker = new AtomicBoolean(false);
new Thread(() -> {
synchronized (locker) {
try {
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread t = new Thread(() -> {
synchronized (locker) {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程[" + Thread.currentThread().getName() + "]阻塞後,狀態:[" + Thread.currentThread().getState() + "]");
}
});
System.out.println("線程[" + t.getName() + "]創建,狀態:[" + t.getState() + "]");
t.start();
System.out.println("線程[" + t.getName() + "]啟動,狀態:[" + t.getState() + "]");
System.out.println("線程[" + t.getName() + "]阻塞中,狀態:[" + t.getState() + "]");
TimeUnit.MILLISECONDS.sleep(5000);
System.out.println("線程[" + t.getName() + "]結束,狀態:[" + t.getState() + "]");
}
}
首先是匿名線程持有locker
,接著線程t啟動,進入RUNNABLE狀態,線程t嘗試獲取locker
,進入BLOCKED狀態,等待後獲取到locker
,進入RUNNABLE狀態,最後執行結束,進入TERMINATED狀態。
阻塞狀態轉換:
等待狀態的轉換
關於等待狀態,Java源碼的註釋有詳細描述如何進入等待狀態,以及如何喚醒處於等待狀態的線程:
Thread state for a waiting thread. A thread is in the waiting state due to calling one of the following methods:
Object.wait with no timeout
Thread.join with no timeout
LockSupport.park
A thread in the waiting state is waiting for another thread to perform a particular action. For example, a thread that has called Object.wait() on an object is waiting for another thread to call Object.notify() or Object.notifyAll() on that object. A thread that has called Thread.join() is waiting for a specified thread to terminate.
WAITING(等待):線程處於等待狀態,處於等待狀態的線程正在等待另一個線程執行的特定操作(通知或中斷)。
再再寫一段代碼:
public class WaitingStateTransition {
public static void main(String[] args) throws InterruptedException {
AtomicBoolean locker = new AtomicBoolean(false);
Thread t = new Thread(() -> {
synchronized (locker) {
try {
TimeUnit.MILLISECONDS.sleep(100);
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程[" + Thread.currentThread().getName() + "]喚醒,狀態:[" + Thread.currentThread().getState() + "]");
}
});
System.out.println("線程[" + t.getName() + "]創建,狀態:[" + t.getState() + "]");
t.start();
System.out.println("線程[" + t.getName() + "]啟動,狀態:[" + t.getState() + "]");
TimeUnit.MILLISECONDS.sleep(150);
System.out.println("線程[" + t.getName() + "]等待,狀態:[" + t.getState() + "]");
new Thread(() -> {
synchronized (locker) {
locker.notify();
}
}).start();
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("線程[" + t.getName() + "]結束,狀態:[" + t.getState() + "]");
}
}
線程t創建後,進入NEW狀態,啟動後,進入RUNNABLE狀態,locker.wait
後,進入WAITING狀態,匿名線程啟動,locker.notify
後,喚醒線程t,進入RUNNABLE狀態,最後線程執行結束,進入TERMINATED狀態。
等待狀態的轉換:
限時等待狀態的轉換
Java源碼的註釋上,也很詳細的解釋瞭如何進入限時等待:
Thread state for a waiting thread with a specified waiting time. A thread is in the timed waiting state due to calling one of the following methods with a specified positive waiting time:
Thread.sleep
Object.wait with timeout
Thread.join with timeout
LockSupport.parkNanos
LockSupport.parkUntil
TIMED_WAITING(限時等待):線程處於限時等待狀態,與等待狀態不同的是,在指定時間後,線程會被自動喚醒。
Tips:也有翻譯成超時等待的,但是我覺得不太合適。
再再再寫一段代碼:
public class TimedWaitingStateTransition {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("線程[" + Thread.currentThread().getName() + "]限時等待後,狀態:[" + Thread.currentThread().getState() + "]");
});
System.out.println("線程[" + t.getName() + "]創建,狀態:[" + t.getState() + "]");
t.start();
System.out.println("線程[" + t.getName() + "]啟動,狀態:[" + t.getState() + "]");
TimeUnit.MILLISECONDS.sleep(50);
System.out.println("線程[" + t.getName() + "]限時等待中,狀態:[" + t.getState() + "]");
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("線程[" + t.getName() + "]結束,狀態:[" + t.getState() + "]");
}
}
線程t創建後,進入NEW狀態,啟動後進入RUNNABLE狀態,線程休眠100ms,進入TIMED_WAITING狀態,休眠時間結束後,進入RUNNABLE狀態,最後線程執行結束,進入TERMINATED狀態。
限時等待狀態的轉換:
線程狀態轉換總結
上面我們通過4段代碼瞭解了線程狀態的轉換,下麵我們通過一張圖來總結下線程的狀態轉換。
結語
今天分享了併發編程的統計數據,因此面試題目較少,不過還是希望對你有幫助。
下一篇內容是剩餘的5個知識點(如果一篇能夠寫完的話):
- Thread類核心方法
- 同步與互斥
- Java線程調度方式
- 死鎖的產生與解決
- 多線程的優點
本篇文章的代碼倉庫:
關註王有志,回覆面試題合集獲取整理後的面試題(正在同步更新,可以持續關註)。
好了,今天就到這裡了,Bye~~