線程池execute 和 submit 的區別

来源:https://www.cnblogs.com/fanchengmeng/archive/2023/04/07/17296716.html
-Advertisement-
Play Games

1. execute 和 submit 的區別 前面說了還需要介紹多線程中使用 execute 和 submit 的區別(這兩個方法都是線程池 ThreadPoolExecutor 的方法)。 1.1 方法來源不同 execute 方法是線程池的頂層介面 Executor 定義的,在 ThreadP ...


1. executesubmit 的區別

前面說了還需要介紹多線程中使用 executesubmit 的區別(這兩個方法都是線程池 ThreadPoolExecutor 的方法)。

1.1 方法來源不同

execute 方法是線程池的頂層介面 Executor 定義的,在 ThreadPoolExecutor 中實現:

void execute(Runnable command);

submit()是在ExecutorService介面中定義的,並定義了三種重載方式:

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

AbstractExecutorService類中有它們的具體實現,而ThreadPoolExecutor繼承了AbstractExecutorService類。所以 ThreadPoolExecutor 也有這三個方法。

1.2 接收參數不同

從上面的方法來源中可以看出,二者接收參數類型不同:

  1. execute()方法只能接收實現Runnable介面類型的任務
  2. submit()方法則既可以接收Runnable類型的任務,也可以接收Callable類型的任務

1.3 返回值不同

由於 RunnableCallable 的區別就是,Runnable 無返回值,Callable 有返回值。

所以 executesubmit 的返回值也不同。

  1. execute()的返回值是void,線程提交後不能得到線程的返回值

  2. submit()的返回值是Future,通過Future的get()方法可以獲取到線程執行的返回值,get()方法是同步的,執行get()方法時,如果線程還沒執行完,會同步等待,直到線程執行完成

    雖然submit()方法可以提交Runnable類型的參數,但執行Future方法的get()時,線程執行完會返回null,不會有實際的返回值,這是因為Runable本來就沒有返回值
    

1.4 異常處理機制不同

  1. submit 提交任務,任務內有異常也不會列印異常信息,而是調用get()方法時,列印出任務執行異常信息
  2. execute 提交任務時,任務內有異常會直接列印出來

後面源碼分析中會體現這個不同點!

2. executesubmit 源碼分析

2.1 submit 源碼分析

submit 方法是 ExecutorService 介面定義,由 AbstractExecutorService 抽象類實現,有三個重載方法:

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

public <T> Future<T> submit(Runnable task, T result) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task, result);
    execute(ftask);
    return ftask;
}

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

可以看一下上面 submit 的三個重載方法,方法體很相似,都調用了一個方法 newTaskFor(...) ,那麼就來看看這個方法,可以看到它有兩個重載方法:

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
    return new FutureTask<T>(runnable, value);
}

protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
    return new FutureTask<T>(callable);
}

解釋一下上面兩個重載方法吧:

  1. 第一個newTaskFor(Runnable runnable, T value):可以看到它應該是將 submit 方法傳進來的 Runnable 轉化成了 Callable,並給一個返回值
  2. 第二個newTaskFor(Callable<T> callable):就是submit直接傳進了一個 Callable,包裝成 FutureTask 返回。

上面代碼中可以看一下 RunnableFutureFutureTask 的關係:

先看一下 RunnableFuture:
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}
RunnableFuture 實現了 Runnable 和 Future,它的子類就是 FutureTask
    
public class FutureTask<V> implements RunnableFuture<V> {
    // ...
}

到這裡就明白了吧,當 submit 傳入的參數是 Runnable 的時候,就需要 FutureTask的構造方法將 Runnable 轉化成 Callable

下麵看一下 FutureTask 的兩個構造函數:

// 傳入Runnable則是執行了一共方法,看一下這個方法,具體轉化邏輯就有了
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

// 傳入Callable直接賦值給類的成員變數
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

下麵看一下當 submit 傳入 Runnable 的時候,其實到這裡就是調用了 FutureTask(Runnable runnable, V result) 構造函數,看一下這個構造函數中將 Runable 轉化成了 Callable,看一下 Executors.callable(runnable, result) 方法:

public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new RunnableAdapter<T>(task, result);
}
看看這裡有創建了一個類,就是 RunnableAdapter,下麵再看一下這個內部類:
    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

看看這個內部類,實現了 Callable 介面,併在 call 方法中 調用了 task 的 run 方法,就是相當於任務代碼直接在 call 方法中了。

這裡必須說一下線程池中的線程是怎麼執行的,這裡就不說全部了,直說與這裡相關的一部分

  1. 看到上面submit 方法最終也是調用了 execute 方法,經過上main源碼分析的一系列轉換,submit最終調用了ThreadPoolExecutorexecute 方法
  2. execute 方法裡面有一個很關鍵的方法是 addWorker(command, true)
  3. 進入 addWorker 方法,可以看到裡面 new 了一個 WorkerWorker 裡面有一個屬性是 Thread,後面直接調用了它的 start 方法啟動了線程
  4. 可以看一下 Worker 類,它實現了 Runnable,這裡就要看看Workerrun 方法了,調用了 runWorker
  5. runWorker方法中,有一行是task.run(),調用 submit 時最終這個run方法就是RunnableFuture中的 run() 方法。具體實現在 FutureTask

下麵就看一下 FutureTask 中實現的 run 方法:

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                //從這裡可以看到,調用了call()方法
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                // 看看,這裡是將異常放在了一個屬性中,所以 submit執行的時候不會拋出異常,只有在調用 get 方法時才會拋出異常
                setException(ex);
            }
            if (ran)
                //這裡將返回值設置到了outcome,執行完後可以通過get()獲取
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

看一下上面兩個主要的方法(setException(ex)set(result)):

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        finishCompletion();
    }
}

可以看到兩個方法一個是將異常放進了 outcome ,一個是將 call 方法的返回值放進了 outcome。不管是異常還是線程執行的返回值,都會在get 方法中獲取到,下麵看一下get 方法,方法在 FutureTask 類中:

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

看看get 方法調用了 report 方法,取到了上面setException(ex)set(result)方法 放進 outcome 的值並返回。

這裡如果線程拋出了異常,這個線程會被從線程哈希表中移除,取消強引用,讓 GC 回收,並且重新創建一個新的線程。

到這裡 submit 方法的源碼就分析完了!!!

2.2 execute 源碼分析

此方法是線程頂層介面 Executor 定義的,在 ThreadPoolExecutor 有其實現,直接看實現:

其實 submit 方法最終調用了 execute 也是這一段,不同的是最後調用的線程的 run 方法是不同實現類實現的

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

這裡主要就是看 submitexecute 的區別點,所以線程池的看具體源碼就不看了,我之前寫的有一篇線程池源碼的筆記很詳細:線程池源碼

上面有個很重要的方法,是將線程加入隊列並執行的,就是 addWorker 方法,這裡就不copy addWorker 的代碼了,只需要知道裡面創建了一個 Worker 對象即可。Worker 對象中有一個屬性是 Thread,後面獲取到了這個 Thread ,並執行了 start 方法。

然而在 Worker 本身是實現了 Runnable 的,所以後面執行的 start 方法,實際是執行了 Worker 中的 run 方法:

public void run() {
    runWorker(this);
}

看看 Workerrun 方法調用的 runWorker 方法:

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        // 這個地方是重點,run 方法
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

其實這裡的源碼就和 submit 一樣了,只是上面 task.run() 調用的是不同實現類的 run 方法。

  • execute 方法傳進來的最終是調用的 Runnable 或其子類的run 方法
  • submit 方法進來的最終是調用了 FutureTaskrun 方法

基於上面的區別,再去看 FutureTaskrun 方法的源碼,就可以知道一下結論:

  1. execute 是沒返回值的,submit 有返回值
  2. execute 提交任務時,任務內有異常會直接列印出來;用 submit 提交任務,任務內有異常也不會列印異常信息,而是調用get()方法時,列印出任務執行異常信息

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

-Advertisement-
Play Games
更多相關文章
  • 本文介紹了學習Spring源碼前需要掌握的核心知識點,包括IOC、AOP、Bean生命周期、初始化和Transaction事務。通過Hello World示例,講解瞭如何使用Spring,並指出了深入瞭解Spring內部機制的方向。 ...
  • 約束(Constraints) 上一章介紹了向模型中添加一些業務邏輯的能力。我們現在可以將按鈕鏈接到業務代碼,但如何防止用戶輸入錯誤的數據?例如,在我們的房地產模塊中,沒有什麼可以阻止用戶設置負預期價格。 odoo提供了兩種設置自動驗證恆定式的方法:Python約束 and SQL約束。 SQL 參 ...
  • Mybatis常見問題 1,大於號、小於號在sql語句中的轉換 使用 mybatis 時 sql 語句是寫在 xml 文件中,如果 sql 中有一些特殊的字元的話,比如< ,<=,>,>=等符號,會引起 xml 格式的錯誤,需要替換掉,或者不被轉義。 有兩種方法可以解決:轉義字元和標記 CDATA ...
  • SpringCloud Eureka-服務註冊與發現02 3.搭建EurekaServer集群-實現負載均衡&故障容錯 3.1為什麼需要集群EurekaServer? 微服務RPC遠程服務調用最核心的是高可用 如果註冊中心只有1個,如果出現故障,會導致整個服務環境不可用 解決辦法就是搭建Eureka ...
  • ps 命令速查備忘清單 Linux我們提供了一個名為 ps 的實用程式,用於查看與系統上的進程相關的信息,它是 Process Status 的縮寫這份 ps 命令備忘清單的快速參考列表,包含常用選項和示例。入門,為開發人員分享快速參考備忘單。 開發速查表大綱 入門 語法 示例 查看系統上的每個進程 ...
  • Perl語言線上運行編譯,是一款可線上編程編輯器,在編輯器上輸入Perl語言代碼,點擊運行,可線上編譯運行Perl語言,Perl語言代碼線上運行調試,Perl語言線上編譯,可快速線上測試您的Perl語言代碼,線上編譯Perl語言代碼發現是否存在錯誤,如果代碼測試通過,將會輸出編譯後的結果。 該線上工 ...
  • 簡介 ChatGPT Java版SDK開源地址:https://github.com/Grt1228/chatgpt-java ,目前收穫將近1000個star。 有bug歡迎朋友們指出,互相學習,所有咨詢全部免費。 最新版:1.0.10 <dependency> <groupId>com.unfb ...
  • 本文分享自天翼雲開發者社區@《Springfox與SpringDoc——swagger如何選擇(SpringDoc入門)》,作者: 才開始學技術的小白 0.引言 之前寫過一篇關於swagger(實際上是springfox)的使用指南(https://www.ctyun.cn/developer/ar ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...