天天看點

大家都說Java有三種建立線程的方式!并發程式設計中的驚天騙局!

作者:JAVA互聯搬磚勞工

一、淺談Java線程的建立方式

回到前面的那個問題,如果是個普通Java程式員,應該會回答“三種”,分别為:

  • ①繼承Thread類;
  • ②實作Runnable接口;
  • ③實作Callable接口。

如果是Pro版的Java程式員,應該會回答“四種”,分别為:

  • ①繼承Thread類;
  • ②實作Runnable接口;
  • ③實作Callable接口;
  • ④使用ExecutorService線程池。

如果是Plus版的Java程式員,應該會回答“五種”,分别為:

  • ①繼承Thread類;
  • ②實作Runnable接口;
  • ③實作Callable接口;
  • ④使用ExecutorService線程池;
  • ⑤使用CompletableFuture類。

如果是ProMax版的Java程式員,應該會回答“七種”,分别為:

  • ①繼承Thread類;
  • ②實作Runnable接口;
  • ③實作Callable接口;
  • ④使用ExecutorService線程池;
  • ⑤使用CompletableFuture類;
  • ⑥基于ThreadGroup線程組;
  • ⑦使用FutureTask類。

如果是超級至尊版的Java程式員,可能還會回答“十種”,分别為:

  • ①繼承Thread類;
  • ②實作Runnable接口;
  • ③實作Callable接口;
  • ④使用ExecutorService線程池;
  • ⑤使用CompletableFuture類;
  • ⑥基于ThreadGroup線程組;
  • ⑦使用FutureTask類;
  • ⑧使用匿名内部類或Lambda表達式;
  • ⑨使用Timer定時器類;
  • ⑩使用ForkJoin線程池或Stream并行流。

如果是……版的Java程式員,或許還會整出十二種、十三種……,但我就不繼續往下羅列了,先簡單将上述提到的十種方式,編寫出相應的代碼。

1.1、繼承Thread類

這是最普通的方式,繼承Thread類,重寫run方法,如下:

java複制代碼public class ExtendsThread extends Thread {
    @Override
    public void run() {
        System.out.println("1......");
    }

    public static void main(String[] args) {
        new ExtendsThread().start();
    }
}
           

1.2、實作Runnable接口

這也是一種常見的方式,實作Runnable接口并重寫run方法,如下:

java複制代碼public class ImplementsRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("2......");
    }

    public static void main(String[] args) {
        ImplementsRunnable runnable = new ImplementsRunnable();
        new Thread(runnable).start();
    }
}
           

想深入研究可以參考之前《Runnable分析》的文章。

1.3、實作Callable接口

和上一種方式類似,隻不過這種方式可以拿到線程執行完的傳回值,如下:

java複制代碼public class ImplementsCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("3......");
        return "zhuZi";
    }

    public static void main(String[] args) throws Exception {
        ImplementsCallable callable = new ImplementsCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}
           

想深入研究可以參考之前《Callable分析》的文章。

1.4、使用ExecutorService線程池

這種屬于進階方式,可以通過Executors建立線程池,也可以自定義線程池,如下:

java複制代碼public class UseExecutorService {
    public static void main(String[] args) {
        ExecutorService poolA = Executors.newFixedThreadPool(2);
        poolA.execute(()->{
            System.out.println("4A......");
        });
        poolA.shutdown();

        // 又或者自定義線程池
        ThreadPoolExecutor poolB = new ThreadPoolExecutor(2, 3, 0,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        poolB.submit(()->{
            System.out.println("4B......");
        });
        poolB.shutdown();
    }
}
           

具體可以參考之前《剖析ThreadPoolExecutor線程池》的文章。

1.5、使用CompletableFuture類

CompletableFuture是JDK1.8引入的新類,可以用來執行異步任務,如下:

java複制代碼public class UseCompletableFuture {
    public static void main(String[] args) throws InterruptedException {
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
            System.out.println("5......");
            return "zhuZi";
        });
        // 需要阻塞,否則看不到結果
        Thread.sleep(1000);
    }
}
           

具體可以參考之前《詳解CompletableFuture》的文章。

1.6、基于ThreadGroup線程組

Java線程可以分組,可以建立多條線程作為一個組,如下:

java複制代碼public class UseThreadGroup {
    public static void main(String[] args) {
        ThreadGroup group = new ThreadGroup("groupName");

        new Thread(group, ()->{
            System.out.println("6-T1......");
        }, "T1").start();

        new Thread(group, ()->{
            System.out.println("6-T2......");
        }, "T2").start();

        new Thread(group, ()->{
            System.out.println("6-T3......");
        }, "T3").start();
    }
}
           

1.7、使用FutureTask類

這個和之前實作Callable接口的方式差不多,隻不過用匿名形式建立Callable,如下:

java複制代碼public class UseFutureTask {
    public static void main(String[] args) {
        FutureTask<String> futureTask = new FutureTask<>(() -> {
            System.out.println("7......");
            return "zhuZi";
        });
        new Thread(futureTask).start();
    }
}
           

想深入研究可以參考之前《剖析FutureTask類》的文章。

1.8、使用匿名内部類或Lambda

這種方式屬于硬扯,就是直接new前面所說的Runnable接口,或者通過Lambda表達式書寫,如下:

java複制代碼public class UseAnonymousClass {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("8A......");
            }
        }).start();

        new Thread(() -> 
                System.out.println("8B......")
        ).start();
    }
}
           

1.9、使用Timer定時器類

在JDK1.3時,曾引入了一個Timer類,用來執行定時任務,如下:

java複制代碼public class UseTimer {
    public static void main(String[] args) {
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("9......");
            }
        }, 0, 1000);
    }
}
           

裡面需要傳入兩個數字,第一個代表啟動後多久開始執行,第二個代表每間隔多久執行一次,機關是ms毫秒。

1.10、使用ForkJoin或Stream并行流

ForkJoin是JDK1.7引入的新線程池,基于分治思想實作。而後續JDK1.8的parallelStream并行流,預設就基于ForkJoin實作,如下:

java複制代碼public class UseForkJoinPool {
    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        forkJoinPool.execute(()->{
            System.out.println("10A......");
        });

        List<String> list = Arrays.asList("10B......");
        list.parallelStream().forEach(System.out::println);
    }
}
           

想要深入研究可以參考《全解ForkJoinPool》的上下兩篇文章。

二、八股文中的驚天騙局

看完前面第一階段,是不是說的頭頭是道?如果你也這樣認為,恭喜你被帶偏了!

不知道從何時起,Java并發程式設計的八股文,在“Java有幾種建立線程的方式”這道題上,開始以“數量”為榮,寫的越多,顯得越專業,越牛X,大家去百度搜個關鍵詞:

“Java有幾種方式建立線程?”

出現的答案,最少都有四種,那這真的對嗎?可以說對,但嚴格意義上來說,又不對。

抛開後面一些先不談,咱們就聊最開始的三種:“繼承Thread類、實作Runnable接口、實作Callable接口”,這應該是廣為人知的答案,不管是剛入行的小白,還是在業内深耕已久的老鳥,相信都背過這一道八股文。

那麼此時來看個例子:

java複制代碼public class ImplementsRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()
                + ":竹子愛熊貓");
    }
}
           

這裡定義了一個類,實作了Runnable接口并重寫了run方法,按前面的說法,這種方式是不是建立了一條線程?答案是Yes,可問題來了,請你告訴我,該如何啟動這條所謂的“線程”呢?

java複制代碼public static void main(String[] args) {
    ImplementsRunnable runnable = new ImplementsRunnable();
    runnable.run();
}
           

難道像上面這樣嘛?來看看運作結果:

css複制代碼main:竹子愛熊貓
           

結果很顯然,列印出的線程名字為:main,代表目前是主線程在運作,和調用普通方法沒任何差別,那究竟該如何建立一條線程呀?要這樣做:

java複制代碼public static void main(String[] args) {
    ImplementsRunnable runnable = new ImplementsRunnable();
    new Thread(runnable).start();
}
           

先new出Runnable對象,接着再new一個Thread對象,然後把Runnable丢給Thread,接着調用start()方法,此時才能真正意義上建立一條線程,運作結果如下:

複制代碼Thread-0:竹子愛熊貓
           

此時線程名字變成了Thread-0,這意味着輸出“竹子愛熊貓”這句話的代碼,并不是main線程在執行了,是以聊到這裡,大家明白我想表達的含義了嘛?實作了Runnable接口的ImplementsRunnable類,并不能被稱為一條線程,包括所謂的Callable、FutureTask……,都不能建立出真正的線程。

換到前面所提出的三種方式中,隻有繼承Thread類,才能真正建立一條線程,如下:

java複制代碼public class ExtendsThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()
                + ":竹子愛熊貓");
    }

    public static void main(String[] args) {
        new ExtendsThread().start();
    }
}

// 運作結果:
//      Thread-0:竹子愛熊貓
           

因為當你用一個類,繼承Thread類時,它内部所有的方法,都會被繼承過來,是以目前類可以直接調用start()方法啟動,更具體點來說,在Java中,建立線程的方式就隻有一種:調用Thread.start()方法!隻有這種形式,才能在真正意義上建立一條線程!

而例如ExecutorService線程池、ForkJoin線程池、CompletableFuture類、Timer定時器類、parallelStream并行流……,如果有去看過它們源碼的小夥伴應該清楚,它們最終都依賴于Thread.start()方法建立線程。

好了,搞清楚這點之後,再回頭來看Runnable、Callable,這倆既然不是建立線程的方式,那它們究竟是什麼?這點咱們放到後面去讨論,先來聊聊“Java有三種建立線程的方式”,這個以訛傳訛的八股文,到底是怎麼來的呢?

究根結底,這個錯誤觀念的源頭,來自于《Java程式設計思想》(《Thinking In Java》)和《Java核心技術》(《Core Java》)這兩本書。在《Core Java》這本書的第12、13章,專門對多線程程式設計進行了講解,提到了四種建立線程的方式:

  • ①繼承Thread類,并重寫run()方法;
  • ②實作Runnable接口,并傳遞給Thread構造器;
  • ③實作Callable接口,建立有傳回值的線程;
  • ④使用Executor架構建立線程池。

同樣的内容,在《Thinking In Java》的第二十一章,也有重複提及到。于是,國内閱讀過這兩本書籍的人,在寫文章、寫面試題、寫書籍、授課、錄視訊……時,把這個概念越傳越泛,按照“三人成虎”原則,Java有3、4種建立線程的方式,這個觀念變成了事實,從此刻在了每個Java開發者的DNA裡。

好了,搞清楚問題的緣由,咱們回到前面提出的問題,既然實作Runnable、Callable接口,不是建立線程的方式,那它們究竟是什麼?準确來說,這是兩種建立“線程體”的方式,包括繼承Thread類重寫run()方法也是。

三、線程與線程體的關系

前面可能提出了一個大家沒接觸過的新概念:線程體,這是個啥?來看看ChatGPT的解釋:

大家都說Java有三種建立線程的方式!并發程式設計中的驚天騙局!

看完這個回答,相信大家就能明白“線程體”是怎麼一回事了,說簡單點,線程是一個獨立的執行單元,可以被作業系統排程;而線程體僅僅隻是一個任務,就類似于一段普通的代碼,需要線程作為載體才能運作,ChatGPT給出的總結特别對:線程是執行線程體的容器,線程體是一個可運作的任務。

不過Java中建立線程體的方式,可以基于Runnable建立,也可以靠Callable建立帶傳回的、也可以通過Timer建立支援定時的……,但不管是哪種方式,到最後都是依賴于Runnable這個類實作的,如果大家有去研究過Callable的原理,大家就會發現:Callable實際上就是Runnable的封裝體。

到這裡,搞清線程與線程體的關系後,相信大家就一定明白了我為何說:Java中建立線程隻有Thread.start()這一種方式的原因了!而最開始給出的其他方式,要麼是在封裝Thread.start(),要麼是在建立線程體,而這個所謂的線程體,更接地氣的說,應該是“多線程任務”。

java複制代碼new Runnable(...);
new Callable(...);
           

這并不是在建立線程,而是建立了兩個可以提供給線程執行的“多線程任務”。

不過還有個問題,任務和線程,到底是怎麼産生綁定關系的呢?大家可以去看Thread類提供的構造器,應該會發現這個構造函數:

java複制代碼public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
           

當new``Thread對象并傳入一個任務時,内部會調用init()方法,把傳入的任務target傳進去,同時還會給線程起個預設名字,即Thread-x,這個x會從0開始(線程名字也可以自定義)。

而當大家去嘗試繼續跟進init()方法時,會發現它在做一系列準備工作,如安全檢測、設定名稱、綁定線程組、設定守護線程……,當init()方法執行完成後,就可以調用Thread.start()方法啟動線程啦。

啟動線程時,最終會調用到start0()這個JNI方法,轉而會去調用JVM的本地方法,即C/C++所編寫的方法,源碼我就不帶着大家去跟了,感興趣的可以去down一下OpenJDK的源碼,或者去搜一下Thread.start()的實作原理,我這裡就大緻總結一下大體過程。

①Thread在類加載階段,就會通過靜态代碼塊去綁定Thread類方法與JVM本地方法的關系:

java複制代碼private static native void registerNatives();
static {
    registerNatives();
}
           

執行完這個registerNatives()本地方法後,Java的線程方法,就和JVM方法綁定了,如start0()這個方法,會對應着JVM_StartThread()這個C++函數等(具體代碼位于openjdk\jdk\src\share\native\java\lang\Thread.c這個檔案)。

②當調用Thread.start()方法後,會先調用Java中定義的start0(),接着會找到與之綁定的JVM_StartThread()這個JVM函數執行(具體實作位于openjdk\hotspot\src\share\vm\prims\jvm.cpp這個檔案)。

③JVM_StartThread()函數最終會調用os::create_thread(...)這個函數,這個函數依舊是JVM函數,畢竟Java要實作跨平台特性,而不同作業系統建立線程的核心函數,也有所差異,如Linux作業系統中,建立線程最終會調用到pthread_create(...)這個核心函數。

④建立出一條核心線程後,接着會去執行Thread::start(...)函數,接着會去執行os::start_thread(thread)這個函數,這一步的作用,主要是讓Java線程,和核心線程産生映射關系,也會在這一步,把Runnable線程體,順勢傳遞給OS的核心線程(具體實作位于openjdk\hotspot\src\share\vm\runtime\Thread.cpp這個檔案)。

⑤當Java線程與核心線程産生映射後,接着就會執行載入的線程體(線程任務),也就是Java程式員所編寫的那個run()方法。

四、總結

看到這裡,這篇文章也就結束了,相較于以往的文章,篇幅方面略顯短小,本文重在糾正大家的錯誤觀念,講述一種學習思維:看任何東西請保持質疑,不要無條件信任别人的說法,要鍛煉自己的深度思考能力,而不是聽風就是雨!學習時請記住這個原則,這才能讓你真正發生質的成長。

最後,如果以後你的面試中,被問到“Java有幾種建立線程的方式”這個問題時,也希望按照本文所說,在面試中聊出與别人不一樣的看法,例如:

Java建立線程有很多種方式啊,像實作Runnable、Callable接口、繼承Thread類、建立線程池等等,不過這些方式并沒有真正建立出線程,嚴格來說,Java就隻有一種方式可以建立線程,那就是通過new Thread().start()建立。

而所謂的Runnable、Callable……對象,這僅僅隻是線程體,也就是提供給線程執行的任務,并不屬于真正的Java線程,它們的執行,最終還是需要依賴于new Thread()……

大家換位思考一下,面試官問别人時,答案都是千篇一律的那三種、四種、五種……,而你能聊出這樣的看法,是不是特别能讓他眼前一亮?是以面試的差異化就來了,别人都是八股文選手,而你擁有着自己的了解,這自然能讓對方給你打上更高的評分,和别人競争Offer時,那不就是手到拈來嘛~

作者:竹子愛熊貓

連結:https://juejin.cn/post/7241395267797942329

繼續閱讀