爲什麼說本質上只有一種實現線程的方式?

爲什麼說本質上只有一種實現線程的方式?

前言

本章主要討論兩個議題

  • 爲什麼說本質上只有一種實現線程的方式?
  • 實現 Runnable 接口究竟比繼承 Thread 類實現線程好在哪裏?

項目環境

1.常見的幾種線程實現方式

很多人可能會說實現 Runnable 接口、繼承 Thread 類,使用線程池創建或者實現 Callable 接口等等,隨隨便便也可以說出 3、4 種,怎麼會是一種呢?下面我們來看看這些實現方式具體是什麼?

1.1 實現 Runnable 接口

static 不是必須的,這裏只是爲了調用測試方便

    static class RunnableThread implements Runnable {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "->第一種:實現 Runnable 接口實現線程");
        }

    }

調用方法:

    public static void main(String[] args) {
        // 第一種 實現 {@link Runnable} 接口
        new Thread(new RunnableThread()).start();
    }

執行結果:

Thread-0->第一種:實現 Runnable 接口實現線程

第 1 種方式是通過實現 Runnable 接口實現多線程,如代碼所示,首先通過 RunnableThread 類實現 Runnable 接口,然後重寫 run() 方法,之後只需要把這個實現了 run() 方法的實例傳到 Thread 類中就可以實現多線程。

1.2 繼承 Thread 類

    static class ExtendsThread extends Thread {

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "->第二種:繼承 Thread 類型實現線程");
        }

    }

調用方法:

    public static void main(String[] args) {
        // 第二種 繼承 {@link Thread}
        new ExtendsThread().start();
    }

執行結果:

Thread-1->第二種:繼承 Thread 類型實現線程

第 2 種方式是繼承 Thread 類,如代碼所示,與第 1 種方式不同的是它沒有實現接口,而是繼承 Thread 類,並重寫 run() 方法。

1.3 使用線程池創建

    private static void threadPoolDemo() {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        executorService.execute(() -> {
            System.out.println(Thread.currentThread().getName() + "->第三種:線程池創建");
        });
    }

執行結果:

pool-1-thread-1->第三種:線程池創建

線程池也是經典的多線程實現,比如示例代碼中我們給線程池的線程數量設置成 3,那麼就會有 3 個子線程來爲我們工作,接下來,我們來看看線程池是怎麼實現線程的?

    /**
     * The default thread factory
     */
    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

對於線程池而言,本質上是通過線程工廠(ThreadFactory)創建線程的,默認採用 DefaultThreadFactory ,它會給線程池創建的線程設置一些默認值,比如:線程的名字、是否是守護線程,以及線程的優先級等。但是無論怎麼設置這些屬性,最終它還是通過 new Thread() 創建線程的 ,只不過這裏的構造函數傳入的參數要多一些,由此可以看出通過線程池創建線程並沒有脫離最開始的那兩種基本的創建方式,因爲本質上還是通過 new Thread() 實現的。

1.4 有返回值的 Callable 創建線程

    private static void callableDemo() {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        Future<String> future = executorService.submit(new CallableTask());
        try {
            System.out.println(future.get(2, TimeUnit.SECONDS));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static class CallableTask implements Callable<String> {

        @Override
        public String call() throws Exception {
            return Thread.currentThread().getName() + "->第四種:實現 Callable 接口";
        }
    }

執行結果:

pool-2-thread-1->第四種:實現 Callable 接口

第 4 種線程創建方式是通過有返回值的 Callable 創建線程,Runnable 創建線程是無返回值的,而 Callable 和與之相關的 Future、FutureTask,它們可以把線程執行的結果作爲返回值返回,如代碼所示,實現了 Callable 接口。

但是,無論是 Callable 還是 FutureTask,它們首先和 Runnable 一樣,都是一個任務,是需要被執行的,而不是說它們本身就是線程。它們可以放到線程池中執行,如代碼所示, submit() 方法把任務放到線程池中,並由線程池創建線程,最終都是靠線程來執行的,而子線程的創建方式仍脫離不了最開始講的兩種基本方式,也就是實現 Runnable 接口和繼承 Thread 類。

1.5 其他方式創建線程

Timer

比如,定時器也可以實現線程,如果新建一個 Timer,令其每隔 10 秒或設置兩個小時之後,執行一些任務,那麼這時它確實也創建了線程並執行了任務,但如果我們深入分析定時器的源碼會發現,本質上它還是會有一個繼承自 Thread 類的 TimerThread,所以定時器創建線程最後又繞回到最開始說的兩種方式(實現 Runnable 接口和繼承 Thread 類)。

匿名內部類

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名內部類");
            }
        }).start();

lambda

        new Thread(() -> {
            System.out.println("lambda 語法");
        }).start();

匿名內部類或 lambda 表達式創建線程,它們僅僅是在語法層面上實現了線程,並不能把它歸結於實現多線程的方式。

2.爲何說實現線程只有一種方式?

以上提到的這幾種其實本質上都符合最開始所說的那兩種實現線程的方式(實現 Runnable 接口和繼承 Thread 類),所以我們只需要搞懂實現 Runnable 接口和繼承 Thread 類這兩種實現線程方式的本質是否一樣就可以了。

首先我們看 Thread#start 方法,start 方法中會調用 start0(); 的方法,這是一個 native 修飾的方法。這一塊涉及到 openjdk 源代碼的具體實現,大致的實現就是 Java 線程調用 start->start0 的方法,實際上會調用到 JVM_StartThread 方法,而 JVM_StartThread 最終調用的是 Java 線程的 run 方法。我們只需要知道 Thread#start 方法最終還是會調用 run 方法就可以了。

Thread#run 方法源碼如下:

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

第 1 行代碼 if (target != null) ,判斷 target 是否等於 null,如果不等於 null,就執行第 2 行代碼 target.run(),而 target 實際上就是一個 Runnable,即使用 Runnable 接口實現線程時傳給Thread類的對象。

我們再看第二種方式,即繼承 Thread 類的方式,繼承 Thread 類之後需要重寫 run 方法,而 start 方法最終還是會調用這個被重寫之後的 run 方法。

綜上所述,本質上創建線程只有一種方式,就是 構造一個 Thread 類,這是創建線程的唯一方式。

而實現 Runnable 接口和繼承 Thread 類這兩種方式的不同點在於實現 run 方法的方式不同,或者說 實現線程運行內容的不同

運行內容主要來自於兩個地方,要麼來自於 target(Runnable 方式),要麼來自於重寫的 run() 方法(Thread方式),在此基礎上進行拓展,可以這樣描述:本質上,實現線程只有一種方式,而要想實現線程執行的內容,卻有兩種方式,也就是可以通過實現 Runnable 接口的方式,或是繼承 Thread 類重寫 run() 方法的方式,把我們想要執行的代碼傳入,讓線程去執行。

3.實現 Runnable 接口比繼承 Thread 類實現線程要好

爲什麼說實現 Runnable 接口比繼承 Thread 類實現線程要好?好在哪裏呢?

  • 首先,我們從代碼的架構考慮,實際上,Runnable 裏只有一個 run() 方法,它定義了需要執行的內容,在這種情況下,實現了 Runnable 與 Thread 類的解耦,Thread 類負責線程啓動和屬性設置等內容,權責分明。

  • 第二點就是在某些情況下可以提高性能,使用繼承 Thread 類方式,每次執行一次任務,都需要新建一個獨立的線程,執行完任務後線程走到生命週期的盡頭被銷燬,如果還想執行這個任務,就必須再新建一個繼承了 Thread 類的類,如果此時執行的內容比較少,比如只是在 run() 方法裏簡單打印一行文字,那麼它所帶來的開銷並不大,相比於整個線程從開始創建到執行完畢被銷燬,這一系列的操作比 run() 方法打印文字本身帶來的開銷要大得多,相當於撿了芝麻丟了西瓜,得不償失。如果我們使用實現 Runnable 接口的方式,就可以把任務直接傳入線程池,使用一些固定的線程來完成任務,不需要每次新建銷燬線程,大大降低了性能開銷。這一點可以參考《線程池的線程複用原理》

  • 第三點在於 Java 語言不支持雙繼承,如果我們的類一旦繼承了 Thread 類,那麼它後續就沒有辦法再繼承其他的類,這樣一來,如果未來這個類需要繼承其他類實現一些功能上的拓展,它就沒有辦法做到了,相當於限制了代碼未來的可拓展性。

綜上所述,我們應該優先選擇通過實現 Runnable 接口的方式來創建線程。

4.參考

  • 《Java 併發編程 78 講》- 徐隆曦
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章