寫在開頭
昨天有個小夥伴私信說自己面試掛在了“Java有幾種建立執行緒的方式”
上,我問他怎麼回答的,他說自己有背過八股文,回答了:繼承Thread類、實現Runnable介面、實現Callable介面、使用執行緒池這四種,但是面試官讓說出8種建立方式,他沒說出來,面試就掛了,面試官給的理由是:只關注八股文背誦,對執行緒的理解不夠深刻!
在這裡想問一下大家,這位小夥伴回答的這四種有問題嗎?看過《Java核心技術卷》和《Java程式設計思想》的朋友應該都知道,在這兩本書中對於多執行緒程式設計都有詳細的介紹,並且也都提到了執行緒建立的方式:
- ①繼承Thread類,並重寫run()方法;
- ②實現Runnable介面,並傳遞給Thread構造器;
- ③實現Callable介面,建立有返回值的執行緒;
- ④使用Executor框架建立執行緒池。
鑑於這兩本書的權威性,以及在國內的廣泛傳播,讓很多學習者,寫書者,教學者都以此為標準,長此以往,這種回答似乎就成了一種看似完美的標準答案了。
因此,這位小夥伴的回答在大部分面試官那裡都是正確的,沒有什麼大問題,但既然這位面試官丟擲了8種的提問,很明顯他要的回答並不是八股文參考答案。那應該怎麼回答才能征服這位面試官呢?請接著往下看!
建立執行緒的10種方式
既然面試官想看執行緒建立的方式,我們就往上整,不僅僅他要的8種,我們還可以說出10種,甚至更多,今天花了點時間,梳理了一下之前用到過得以及網上看到的執行緒建立的辦法,我們透過一個個小demo去感受一下。🥰
① 繼承Thread類,並重寫run()方法
這是最基本的一個執行緒建立的方式,閒話少敘,直接上程式碼!
【程式碼示例1】
public class Test {
public static void main(String[] args) {
new ThreadTest().start();
}
}
//繼承 Thread,重寫 run() 方法
class ThreadTest extends Thread {
@Override
public void run() {
for (int i = 0; i <3; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
輸出:
Thread-0:0
Thread-0:1
Thread-0:2
建立一個ThreadTest 並繼承Thread類,重寫run方法,來建立一個執行緒,當然我們還可以採用匿名內部類去重寫run方法來建立執行緒,這其實也可以算所一種方式
【程式碼示例2】
public class Test {
public static void main(String[] args) {
new Thread("t1"){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}.start();
}
}
//列印結果:t1
② 實現Runnable介面
這也是常用的四個方式之一,實現Runnable介面並重寫run方法。
【程式碼示例3】
public class Test implements Runnable{
public static void main(String[] args) {
Test test = new Test();
new Thread(test).start();
}
@Override
public void run() {
System.out.println("我是Runnable執行緒");
}
}
//列印結果:我是Runnable執行緒
③ 實現Callable介面
這種方式實現Callable介面,可以建立有返回值的執行緒。
【程式碼示例4】
public class Test implements Callable<String> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Test test = new Test();
FutureTask<String> stringFutureTask = new FutureTask<>(test);
new Thread(stringFutureTask).start();
System.out.println(stringFutureTask.get());
}
@Override
public String call() throws Exception {
return "我是執行緒Callable";
}
}
//列印結果:我是執行緒Callable
這個示例裡使用了FutureTask
,這個類可用於非同步獲取執行結果或取消執行任務的場景。透過傳入Runnable或者Callable的任務給FutureTask,直接呼叫其run方法或者放入執行緒池執行,之後可以在外部透過FutureTask的get方法非同步獲取執行結果。
④ 使用ExecutorService執行緒池
透過Executors建立執行緒池,Executors 類是從 JDK 1.5 開始就新增的執行緒池建立的靜態工廠類,它就是建立執行緒池的,但是很多的大廠已經不建議使用該類去建立執行緒池。原因在於,該類建立的很多執行緒池的內部使用了無界任務佇列,在併發量很大的情況下會導致 JVM 丟擲 OutOfMemoryError,直接讓 JVM 崩潰,影響嚴重。因此,在這裡我們只將它作為一個案例參考,真實開發中不建議使用!
【程式碼示例5】
public class Test {
public static void main(String[] args) {
// 使用工具類 Executors 建立單執行緒執行緒池,其實還有其他幾種建立方式
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
//提交執行任務
singleThreadExecutor.submit(() -> {System.out.println("單執行緒執行緒池執行任務");});
//關閉執行緒池
singleThreadExecutor.shutdown();
}
}
//列印結果:單執行緒執行緒池執行任務
⑤ 使用CompletableFuture類
CompletableFuture是JDK1.8引入的新類,CompletableFuture 除了提供了更為好用和強大的 Future 特性之外,還提供了函數語言程式設計、非同步任務編排組合(可以將多個非同步任務串聯起來,組成一個完整的鏈式呼叫)等能力。後面的文章更新中會詳說,現在先上程式碼!
【程式碼示例6】
public class Test {
public static void main(String[] args) throws InterruptedException {
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + ":"+"CompletableFuture");
return "CompletableFuture";
});
// 需要阻塞,否則看不到結果
Thread.sleep(1000);
}
}
//列印結果:ForkJoinPool.commonPool-worker-1:CompletableFuture
⑥ 基於ThreadGroup執行緒組
在Java的執行緒中同樣有組的概念,可以透過ThreadGroup建立一個執行緒組,線上程組中建立多個執行緒。
【程式碼示例7】
public class Test {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("groupName");
new Thread(group, ()->{
System.out.println("T1......");
}, "T1").start();
new Thread(group, ()->{
System.out.println("T2......");
}, "T2").start();
new Thread(group, ()->{
System.out.println("T3......");
}, "T3").start();
}
}
輸出:
T1......
T2......
T3......
⑦ 使用FutureTask類
看到這個FutureTask類是不是很熟悉,對嘍!咱們在第三種方式,實現Callable介面,重寫call方法中也用到了它,他們的實現方式幾乎都萬變不離其宗,只不過我們在這裡採用了lambda 表示式呼叫。
【程式碼示例8】
public class Test {
public static void main(String[] args) {
FutureTask<String> futureTask = new FutureTask<>(() -> {
System.out.println(Thread.currentThread().getName()+":"+"futureTask");
return "futureTask";
});
new Thread(futureTask).start();
}
}
//執行結果:Thread-0:futureTask
其實雖然是匿名方式,它的底部仍然呼叫了callable。我們來看一下FutureTask底層的構造方法,都是透過傳參或者呼叫callable。
【原始碼解析1】
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
// 透過介面卡RunnableAdapter來將Runnable物件runnable轉換成Callable物件
this.callable = Executors.callable(runnable, result);
this.state = NEW;
}
⑧ 使用匿名內部類或Lambda表示式
這種方式其實在上面的實現中多少都有提到,匿名方式建立,lambda 表示式建立。
【程式碼示例9】
public class Test {
public static void main(String[] args) {
//new Runnable 物件,匿名重寫 run() 方法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("匿名建立執行緒");
}
}).start();
//JDK 1.8 開始支援 lambda 表示式
new Thread(() ->
System.out.println("lambda建立執行緒")
).start();
}
}
⑨ 使用Timer定時器類
Timer類在JDK1.3時被引入,用來執行定時任務,裡面需要傳入兩個數字,第一個代表啟動後多久開始執行,第二個代表每間隔多久執行一次,單位是ms毫秒。
【程式碼示例10】
public class Test {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("定時器執行緒");
}
}, 0, 1000);
}
}
⑩ 使用ForkJoin執行緒池或Stream並行流
ForkJoin是JDK1.7引入的新執行緒池,基於分治思想實現。而後續JDK1.8的parallelStream並行流,預設就基於ForkJoin實現,我們直接上程式碼感受一下。
【程式碼示例11】
public class Test {
public static void main(String[] args) {
//ForkJoinPool執行緒池
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.execute(()->{
System.out.println(Thread.currentThread().getName()+":"+"ForkJoinPool執行緒池");
});
//parallelStream流
List<String> list = Arrays.asList(Thread.currentThread().getName()+":"+"parallelStream流");
list.parallelStream().forEach(System.out::println);
}
}
輸出:
ForkJoinPool-1-worker-1:ForkJoinPool執行緒池
//並行流在主執行緒中被列印。
main:parallelStream流
總結
OK,我們根據面試官的需求,寫出了10種建立執行緒的方式,如果再細分,甚至還可以更多,畢竟執行緒池的工具類還有沒往上寫的呢。
那麼,我們一起靜默3分鐘,好好思考一下,在Java中建立一個執行緒的本質,真的是八股文中所說的3種、4種、8種,甚至更多嗎?Build哥認為,真正建立執行緒的方式只有1種,其他的衍生品都算套殼!
考慮到本篇已經六七千字了,所以我們在下一篇文章中來分析一下為什麼“真正建立執行緒的方式只有1種!”
結尾彩蛋
如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!