實現線程的方式到底有幾種?

這篇文章主要講解實現線程的方式到底有幾種?以及實現 Runnable 接口究竟比繼承 Thread 類實現線程好在哪裏?編程

實現線程是併發編程中基礎中的基礎,由於咱們必需要先實現多線程,才能夠繼續後續的一系列操做。因此本文就先從併發編程的基礎如何實現線程開始講起。多線程

實現線程的方式到底有幾種?咱們接下來看看它們具體指什麼?架構

實現 Runnable 接口

public class RunnableThread implements Runnable {

    @Override
    public void run() {
        System.out.println("實現Runnable接口實現線程");
    }
}

第 1 種方式是經過實現 Runnable 接口實現多線程,如代碼所示,首先經過 RunnableThread 類實現 Runnable 接口,而後重寫 run() 方法,以後只須要把這個實現了 run() 方法的實例傳到 Thread 類中就能夠實現多線程。併發

繼承 Thread 類

public class ExtendsThread extends Thread {
     
    @Override
    public void run() {
        System.out.println(「繼承Thread類實現線程");
    }
}

第 2 種方式是繼承 Thread 類,如代碼所示,與第 1 種方式不一樣的是它沒有實現接口,而是繼承 Thread 類,並重寫了其中的 run() 方法。相信上面這兩種方式你必定很是熟悉,而且常常在工做中使用它們。dom

線程池建立線程

那麼爲何說還有第 3 種或第 4 種方式呢?咱們先來看看第 3 種方式:經過線程池建立線程。線程池確實實現了多線程,好比咱們給線程池的線程數量設置成 10,那麼就會有 10 個子線程來爲咱們工做,接下來,咱們深刻解析線程池中的源碼,來看看線程池是怎麼實現線程的?ide

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;
    }
}

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

除此以外,Callable 也是能夠建立線程的,可是本質上也是經過前兩種基本方式實現的線程建立。oop

有返回值的 Callable 建立線程

class CallableTask 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 CallableTask());

第 4 種線程建立方式是經過有返回值的 Callable 建立線程,Runnable 建立線程是無返回值的,而 Callable 和與之相關的 Future、FutureTask,它們能夠把線程執行的結果做爲返回值返回,如代碼所示,實現了 Callable 接口,而且給它的泛型設置成 Integer,而後它會返回一個隨機數。性能

可是,不管是 Callable 仍是 FutureTask,它們首先和 Runnable 同樣,都是一個任務,是須要被執行的,而不是說它們自己就是線程。它們能夠放到線程池中執行,如代碼所示, submit() 方法把任務放到線程池中,並由線程池建立線程,無論用什麼方法,最終都是靠線程來執行的,而子線程的建立方式仍脫離不了最開始講的兩種基本方式,也就是實現 Runnable 接口和繼承 Thread 類。學習

除了上述經常使用的實現線程的方式還有如下方式:

定時器 Timer

class TimerThread extends Thread {

    boolean newTasksMayBeScheduled = true;

    private TaskQueue queue;

    TimerThread(TaskQueue queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            mainLoop();
        } finally {
            synchronized(queue) {
                newTasksMayBeScheduled = false;
                queue.clear();
            }
        }
    }

    private void mainLoop() {
        while (true) {
            try {
                TimerTask task;
                boolean taskFired;
                synchronized(queue) {
                    while (queue.isEmpty() && newTasksMayBeScheduled)
                        queue.wait();
                    if (queue.isEmpty())
                        break;

                    long currentTime, executionTime;
                    task = queue.getMin();
                    synchronized(task.lock) {
                        if (task.state == TimerTask.CANCELLED) {
                            queue.removeMin();
                            continue;
                        }
                        currentTime = System.currentTimeMillis();
                        executionTime = task.nextExecutionTime;
                        if (taskFired = (executionTime<=currentTime)) {
                            if (task.period == 0) {
                                queue.removeMin();
                                task.state = TimerTask.EXECUTED;
                            } else {
                                queue.rescheduleMin(
                                  task.period<0 ? currentTime   - task.period
                                                : executionTime + task.period);
                            }
                        }
                    }
                    if (!taskFired)
                        queue.wait(executionTime - currentTime);
                }
                if (taskFired)
                    task.run();
            } catch(InterruptedException e) {
            }
        }
    }
}

定時器也能夠實現線程,若是新建一個 Timer,令其每隔 10 秒或設置兩個小時以後,執行一些任務,那麼這時它確實也建立了線程並執行了任務,但若是咱們深刻分析定時器的源碼會發現,本質上它仍是會有一個繼承自 Thread 類的 TimerThread,因此定時器建立線程最後又繞回到最開始說的兩種方式。

匿名內部類建立線程

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}).start();

還有經過匿名內部類或 lambda 表達式方式來建立線程,實際上,匿名內部類或 lambda 表達式建立線程,它們僅僅是在語法層面上實現了線程,並不能把它歸結於實現多線程的方式,如匿名內部類實現線程的代碼所示,它僅僅是用一個匿名內部類把須要傳入的 Runnable 給實例出來。

new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

咱們再來看下 lambda 表達式方式。如代碼所示,最終它們依然符合最開始所說的那兩種實現線程的方式。

實現線程只有一種方式

咱們先不認爲建立線程只有一種方式,先認爲有兩種建立線程的方式,而其餘的建立方式,好比線程池或是定時器,它們僅僅是在 new Thread() 外作了一層封裝,若是咱們把這些都叫做一種新的方式,那麼建立線程的方式便會變幻無窮、層出不窮,好比 JDK 更新了,它可能會多出幾個類,會把 new Thread() 從新封裝,表面上看又會是一種新的實現線程的方式,透過現象看本質,打開封裝後,會發現它們最終都是基於 Runnable 接口或繼承 Thread 類實現的。

接下來,咱們進行更深層次的探討,爲何說這兩種方式本質上是一種呢?

首先,啓動線程須要調用 start() 方法,而 start() 方法最終還會調用 run() 方法,咱們先來看看第一種方式中 run() 方法到底是怎麼實現的:

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

能夠看出 run() 方法的代碼很是短小精悍,第 1 行代碼 if (target != null) ,判斷 target 是否等於 null,若是不等於 null,就執行第 2 行代碼 target.run(),而 target 實際上就是一個 Runnable,即便用 Runnable 接口實現線程時傳給 Thread 類的對象。

而後,咱們來看第二種方式,也就是繼承 Thread 方式,實際上,繼承 Thread 類以後,會把上述的 run() 方法重寫,重寫後 run() 方法裏直接就是所須要執行的任務,但它最終仍是須要調用 thread.start() 方法來啓動線程,而 start() 方法最終也會調用這個已經被重寫的 run() 方法來執行它的任務,這時咱們就能夠完全明白了,事實上建立線程只有一種方式,就是構造一個 Thread 類,這是建立線程的惟一方式。

咱們上面已經瞭解了兩種建立線程方式本質上是同樣的,它們的不一樣點僅僅在於實現線程運行內容的不一樣,那麼運行內容來自於哪裏呢?

運行內容主要來自於兩個地方,要麼來自於 target,要麼來自於重寫的 run() 方法,在此基礎上咱們進行拓展,能夠這樣描述:本質上,實現線程只有一種方式,而要想實現線程執行的內容,卻有兩種方式,也就是能夠經過實現 Runnable 接口的方式,或是繼承 Thread 類重寫 run() 方法的方式,把咱們想要執行的代碼傳入,讓線程去執行,在此基礎上,若是咱們還想有更多實現線程的方式,好比線程池和 Timer 定時器,只須要在此基礎上進行封裝便可。

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

下面咱們來對剛纔說的兩種實現線程內容的方式進行對比,也就是爲何說實現 Runnable 接口比繼承 Thread 類實現線程要好?好在哪裏呢?

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

第二點就是在某些狀況下能夠提升性能,使用繼承 Thread 類方式,每次執行一次任務,都須要新建一個獨立的線程,執行完任務後線程走到生命週期的盡頭被銷燬,若是還想執行這個任務,就必須再新建一個繼承了 Thread 類的類,若是此時執行的內容比較少,好比只是在 run() 方法裏簡單打印一行文字,那麼它所帶來的開銷並不大,相比於整個線程從開始建立到執行完畢被銷燬,這一系列的操做比 run() 方法打印文字自己帶來的開銷要大得多,至關於撿了芝麻丟了西瓜,得不償失。若是咱們使用實現 Runnable 接口的方式,就能夠把任務直接傳入線程池,使用一些固定的線程來完成任務,不須要每次新建銷燬線程,大大下降了性能開銷。

第三點好處在於 Java 語言不支持雙繼承,若是咱們的類一旦繼承了 Thread 類,那麼它後續就沒有辦法再繼承其餘的類,這樣一來,若是將來這個類須要繼承其餘類實現一些功能上的拓展,它就沒有辦法作到了,至關於限制了代碼將來的可拓展性。

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

總結

本文主要學習了經過 Runnable 接口和繼承 Thread 類等幾種方式建立線程,又詳細分析了爲何說本質上只有一種實現線程的方式,以及實現 Runnable 接口究竟比繼承 Thread 類實現線程好在哪裏?看完本文相信你必定對建立線程有了更深刻的理解。

相關文章
相關標籤/搜索