面試官讓說出8種建立執行緒的方式,我只說了4種,然後掛了。。。

JavaBuild發表於2024-03-07

寫在開頭

昨天有個小夥伴私信說自己面試掛在了“Java有幾種建立執行緒的方式”上,我問他怎麼回答的,他說自己有背過八股文,回答了:繼承Thread類、實現Runnable介面、實現Callable介面、使用執行緒池這四種,但是面試官讓說出8種建立方式,他沒說出來,面試就掛了,面試官給的理由是:只關注八股文背誦,對執行緒的理解不夠深刻!

在這裡想問一下大家,這位小夥伴回答的這四種有問題嗎?看過《Java核心技術卷》和《Java程式設計思想》的朋友應該都知道,在這兩本書中對於多執行緒程式設計都有詳細的介紹,並且也都提到了執行緒建立的方式:

  • ①繼承Thread類,並重寫run()方法;
  • ②實現Runnable介面,並傳遞給Thread構造器;
  • ③實現Callable介面,建立有返回值的執行緒;
  • ④使用Executor框架建立執行緒池。

鑑於這兩本書的權威性,以及在國內的廣泛傳播,讓很多學習者,寫書者,教學者都以此為標準,長此以往,這種回答似乎就成了一種看似完美的標準答案了。

因此,這位小夥伴的回答在大部分面試官那裡都是正確的,沒有什麼大問題,但既然這位面試官丟擲了8種的提問,很明顯他要的回答並不是八股文參考答案。那應該怎麼回答才能征服這位面試官呢?請接著往下看!

建立執行緒的10種方式

既然面試官想看執行緒建立的方式,我們就往上整,不僅僅他要的8種,我們還可以說出10種,甚至更多,今天花了點時間,梳理了一下之前用到過得以及網上看到的執行緒建立的辦法,我們透過一個個小demo去感受一下。🥰
image

① 繼承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哥!
image
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!
image

相關文章