工做三年,小胖竟然問我:建立線程有幾種方式?

一個問題

哈嘍,我是狗哥。話很少說,金三銀四,不少同窗立刻就要參加春招了。而多線程確定是面試必問的,開篇以前,問你們一個問題:建立線程到底有幾種方式?前端

  • 基礎答案(回答錯誤):兩種,繼承 Thread 和 實現 Runnable
  • 進階答案(回答錯誤):多種,繼承 Thread 、實現 Runnable、線程池建立、Callable 建立、Timer 建立等等

相信以上答案不少同窗都能答出來。但它們都是錯誤的,其實建立線程的方式只有一種。爲何?狗哥你丫逗我麼?橫看豎看,至少也得兩種呀。別急,放下刀。且聽我慢慢分析:java

第一種:繼承 Thread

首先是繼承 Thread,建立線程最經典的方法,這種方法很常見啦。剛入門的時候,狗哥寫過不知道多少遍了。它的寫法是這樣的:面試

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("經過集成 Thread 類實現線程");
    }

}

// 如何使用
new MyThread().start()

如代碼所示:繼承 Thread 類,並重寫了其中的 run () 方法,以後直接調用 start() 便可實現多線程。相信上面這種方式你必定很是熟悉,而且常常在工做中使用它們。算法

第二種:實現 Runnable

也是最經常使用的方法,寫法以下:數據庫

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("經過實現 Runnable 方式實現線程");
    }

}

// 使用
// 一、建立MyRunnable實例
MyRunnable runnable = new MyRunnable();
//2.建立Thread對象
//3.將MyRunnable放入Thread實例中
Thread thread = new Thread(runnable);
//4.經過線程對象操做線程(運行、中止)
thread.start();

如代碼所示,這種方法實際上是定義一個線程執行的任務(run 方法裏面的邏輯)並無建立線程。它首先經過 MyRunnable類實現 Runnable 接口,而後重寫 run () 方法,以後還要把這個實現了 run () 方法的實例傳到 Thread 類中才能夠實現多線程編程

第三種:線程池建立線程

說完這兩種在工做中最經常使用的,咱們再說說第三種。在 Java 中,咱們建立線程池是這樣的:設計模式

// 10 是核心線程數量
ExecutorService service = Executors.newFixedThreadPool(10);

點進去 newFixedThreadPool 源碼,在 IDEA 中調試,能夠發現它的調用鏈是這樣的:微信

Executors.newFixedThreadPool(10) --> new ThreadPoolExecutor(一堆參數) --> Executors.defaultThreadFactory()

能夠發現最終仍是調用了 Executors.defaultThreadFactory() 方法,而這個方法的源碼是這樣的:數據結構

static class DefaultThreadFactory implements ThreadFactory {
    // 線程池序號
    static final AtomicInteger poolNumber = new AtomicInteger(1);
    // 線程序號
    final AtomicInteger threadNumber = new AtomicInteger(1);
    // 線程組
    final ThreadGroup group;
    // 線程池前綴
    final String namePrefix;

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

    /**
     * 重點方法
     * @param r
     * @return
     */
    @Override
    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;
    }
}

如上源碼所示:線程池建立線程本質上是默認經過 DefaultThreadFactory 線程工廠來建立的。它能夠設置線程的一些屬性,好比:是否守護線程、優先級、線程名、等等。多線程

但不管怎麼設置,最終它仍是須要經過 new Thread () 建立線程的。因此線程池建立線程並無脫離以上的兩種基本的建立方式

第四種:Callable 建立

第四種是有返回值的 Callable 建立線程,用法是這樣的:

public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        return new Random().nextInt();
    }

    // 使用方法
    // 一、建立線程池
    ExecutorService service = Executors.newFixedThreadPool(10);
    // 二、提交任務,並用 Future提交返回結果
    Future< Integer > future = service.submit(new MyCallable());

}

Callable 與 Runnable 名字還有點像,區別在於 Runnable 是無返回值的。它們的本質都是定義線程要作的任務(call 或 run 方法裏面的邏輯),而不是說他們自己就是線程。但不管有無返回值,它們都是須要被線程執行。

如代碼所示,它們能夠提交到線程池執行,經過 sumbit 方法提交。這時就參考方式三,由線程工廠負責建立線程。固然,還有其餘方法執行 Callable 任務。可是無論怎麼說,它仍是離不開實現 Runnable 接口和繼承 Thread 類這兩種方式

第五種:Timer 建立

咱們使用 Timer 的方式以下:

public class MyTimer {

    public static void main(String[] args) {
        timer();
    }

    /**
     * 指定時間 time 執行 schedule(TimerTask task, Date time)
     */
    public static void timer() {
        Timer timer = new Timer();
        // 設定指定的時間time,此處爲2000毫秒
        timer.schedule(new TimerTask() {
            public void run() {
                System.out.println("執行定時任務");
            }
        }, 2000);
    }

}

如代碼所示,Timer 定時器在兩秒以後執行一些任務,它也確實建立了線程,可是深刻源碼:

private final TimerThread thread = new TimerThread(queue);

public Timer() {
    this("Timer-" + serialNumber());
}

public Timer(String name) {
    thread.setName(name);
    thread.start();
}

class TimerThread extends Thread {
    // 省略內部方法
}

注意到 TimerThread ,它仍是繼承於 Thread ,因此 Timer 建立線程最後又繞回到最開始說的兩種方式了。

爲何只有一種方式?

有同窗可能說,狗哥你這扯半天不仍是兩種方式麼?我答對了呀。。。別急,容我喝口水,下面分析爲什麼說它是一種?

注意到 Thread 類中有一個 run 方法:

private Runnable target;

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

先看實現 Runnable 方式,它啓動線程仍是須要調用 start 方法(由於是 Native 方法咱們看不到具體邏輯),可是線程要執行任務必須仍是要調用 run 方法(否則線程執行的是啥?)。

咱們看代碼,run 方法很是簡單。它判斷 target 不爲 null 就直接執行 target 的 run 方法。而 target 正是咱們實現的 Runnable ,使用 Runnable 接口實現線程時傳給 Thread 類的對象

在看繼承 Thread 方式,它調用 thread.start(),最終調用的仍是 run 方法(run() 裏面是任務)。只不過這個 run() 是咱們已經重寫的 run() 而不是上面 Runnable(target) 的 run()。

看到這裏可算明白了,事實上建立線程本質只有一種方式,就是構造一個 Thread 類,這是建立線程的惟一方式,不一樣的只是 run 方法(執行內容)的實現方式

任務的來源

一、2 兩種方式它們的不一樣點僅僅在於實現線程執行內容的不一樣,那麼運行內容來自於哪裏呢?

本質上,實現線程只有一種方式,而要想實現線程執行的內容,卻有兩種寫法:

  • 實現 Runnable 接口從而實現 run() 的方式
  • 繼承 Thread 類重寫 run () 方法的方式

而後把咱們想要執行的代碼傳入,讓線程去執行。在此基礎上,若是咱們還想有更多實現線程的方式,好比線程池、Callable 以及 Timer 定時器,只須要在此基礎上進行封裝便可

哪一種寫法好?

答案是:Runnable 寫法

理由:

  • 易於擴展

這個很少說,Java 是單繼承。若是使用繼承 Thread 的寫法。將不利於後續擴展。

  • 解耦

用 Runnable 負責定義 run() 方法(執行內容)。這種狀況下,它與 Thread 實現瞭解耦。Thread 負責線程的啓動以及相關屬性設置。

  • 性能

在一些狀況下能夠提升性能。好比:線程執行的內容很簡單,就是打印個日誌。若是使用 Thread 實現,那它會從線程建立到銷燬都要走一遍,須要屢次執行時,還須要屢次走這重複的流程,內存開銷很是大。

可是咱們使用 Runnable 就不同了。能夠把它扔到線程池裏面,用固定的線程執行。這樣,顯然是能夠提升效率的。

巨人的肩膀

  • Java 併發編程 78 講 — 徐隆曦

福利

若是看到這裏,喜歡這篇文章的話,請幫點個好看。微信搜索一個優秀的廢人,關注後回覆電子書送你 100+ 本編程電子書 ,不僅 Java 哦,詳情看下圖。回覆1024送你一套完整的 java 視頻教程。

資源

C語言

C++

Java

Git

Python

GO

Linux

經典必讀

面試相關

前端

人工智能

設計模式

數據庫

數據結構與算法

計算機基礎

一個優秀的廢人

相關文章
相關標籤/搜索