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

来源:https://www.cnblogs.com/wupeixuan/archive/2020/02/03/12256190.html
-Advertisement-
Play Games

這篇文章主要講解實現線程的方式到底有幾種?以及實現 Runnable 介面究竟比繼承 Thread 類實現線程好在哪裡? 實現線程是併發編程中基礎中的基礎,因為我們必須要先實現多線程,才可以繼續後續的一系列操作。所以本文就先從併發編程的基礎如何實現線程開始講起。 實現線程的方式到底有幾種?我們接下來 ...


這篇文章主要講解實現線程的方式到底有幾種?以及實現 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() 方法。相信上面這兩種方式你一定非常熟悉,並且經常在工作中使用它們。

線程池創建線程

那麼為什麼說還有第 3 種或第 4 種方式呢?我們先來看看第 3 種方式:通過線程池創建線程。線程池確實實現了多線程,比如我們給線程池的線程數量設置成 10,那麼就會有 10 個子線程來為我們工作,接下來,我們深入解析線程池中的源碼,來看看線程池是怎麼實現線程的?

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 也是可以創建線程的,但是本質上也是通過前兩種基本方式實現的線程創建。

有返回值的 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 類實現線程好在哪裡?看完本文相信你一定對創建線程有了更深入的理解。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 網路原理是工程師的必須瞭解的電腦基礎知識,先推薦下兩本好書,《圖解HTTP》和《圖解TCP/IP》。 《圖解TCP/IP》講解網路基礎知識、TCP/IP基礎知識、數據鏈路、IP協議、IP協議相關技術、TCP與UDP、路由協議、應用協議、網路安全等內容,《圖解HTTP》對HTTP協議進行了全面系統的 ...
  • 時間序列資料庫(TSDB)初識與選擇 本文作者由 MageByte 團隊的 「借來方向」編寫,關註公眾號 給你更多硬核技術 背景 這兩年互聯網行業掀著一股新風,總是聽著各種高大上的新名詞。大數據、人工智慧、物聯網、機器學習、商業智能、智能預警啊等等。 以前的系統,做數據可視化,信息管理,流程式控制制。現 ...
  • Redis詳解(六)——哨兵機制 一、概述 Redis Sentinel是一個分散式系統,為Redis提供高可用性解決方案。可以在一個架構中運行多個 Sentinel 進程(progress), 這些進程使用流言協議(gossip protocols)來 接收關於主伺服器是否下線的信息, 並使用投票 ...
  • SublimeREPL插件 這個是首先要安裝的,此插件主要功能是為了實現交互,在安裝後需要一些簡單的配置 在Preferences Key Bindings user下添加如下代碼設置快捷鍵 { "keys": ["f5"],//可以自己改變 "caption": "SublimeREPL: Pyt ...
  • 本文介紹通過java程式在excel中操作形狀(圖形)的方法,包括: 1. 添加形狀(如設置形狀類型/位置/大小、形狀顏色填充(單色/漸變色/紋理/圖片填充)、形狀顯示或隱藏、形狀傾斜角度、添加文本到形狀、形狀陰影等) 2. 讀取形狀中的文本和圖片 3. 刪除形狀(刪除指定或全部形狀) 工具:Spi ...
  • MyBatis是一個Java的持久層框架,和Hibernate一樣,都是ORM框架。 MyBaits的前身是iBatis,可以將資料庫中記錄映射為pojo,是開發人員以面向對象編程的思想來操作資料庫。 MyBatis消除了幾乎所有的JDBC代碼、參數的手動設置、對結果集的檢索,開發人員只需要使用xm ...
  • 之前寫的那篇 "Spring框架學習筆記(5)——Spring Boot創建與使用" ,發現有多小細節沒有提及,,正好現在又學習了mybatis plus這款框架,打算重新整理一遍,並將細節說清楚 1.通過IDEA創建spring boot 2.項目相關配置 只需要修改第一個和第二個,下麵的其他選項 ...
  • 一、前言 從研究生開始到工作半年,陸續在接觸MCU SOC這些以CPU為核心的控制器,但由於專業的原因一直對CPU的內部結構和工作原理一知半解。今天從一篇博客中打破一直以來的盲區。特此聲明,本文設計思想及代碼均源於如下博文,這裡僅用於自己學習記錄,以及分享心得之用。 簡易CPU的設計和實現_阡飛陌- ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...