1. 程式人生 > >深入原始碼分析Java執行緒池的實現原理

深入原始碼分析Java執行緒池的實現原理

程式的執行,其本質上,是對系統資源(CPU、記憶體、磁碟、網路等等)的使用。如何高效的使用這些資源是我們程式設計優化演進的一個方向。今天說的執行緒池就是一種對CPU利用的優化手段。

網上有不少介紹如何使用執行緒池的文章,那我想說點什麼呢?我希望通過學習執行緒池原理,明白所有池化技術的基本設計思路。遇到其他相似問題可以解決。

池化技術

前面提到一個名詞——池化技術,那麼到底什麼是池化技術呢?

池化技術簡單點來說,就是提前儲存大量的資源,以備不時之需。在機器資源有限的情況下,使用池化技術可以大大的提高資源的利用率,提升效能等。

在程式設計領域,比較典型的池化技術有:

執行緒池、連線池、記憶體池、物件池等。

本文主要來介紹一下其中比較簡單的執行緒池的實現原理,希望讀者們可以舉一反三,通過對執行緒池的理解,學習並掌握所有程式設計中池化技術的底層原理。

建立一個執行緒

在Java的併發程式設計中,執行緒是十分重要的,在Java中,建立一個執行緒比較簡單:

public class App {
    public static void main(String[] args) throws Exception {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("執行緒執行中");
            }
        }).start();
    }
}

我們通過建立一個執行緒物件,並且實現Runnable介面就可以實現一個簡單的執行緒。可以利用上多核CPU。當一個任務結束,當前執行緒就接收。

但很多時候,我們不止會執行一個任務。如果每次都是如此的建立執行緒->執行任務->銷燬執行緒,會造成很大的效能開銷。

那能否一個執行緒建立後,執行完一個任務後,又去執行另一個任務,而不是銷燬。這就是執行緒池。

這也就是池化技術的思想,通過預先建立好多個執行緒,放在池中,這樣可以在需要使用執行緒的時候直接獲取,避免多次重複建立、銷燬帶來的開銷。

執行緒池的簡單使用

以下程式碼,是在Java中建立執行緒池:

import java.util.concurrent.*;

public class App {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = new ThreadPoolExecutor(1, 1,
                60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10));

        executorService.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("abcdefg");
            }
        });

        executorService.shutdown();
    }
}

Jdk提供給外部的介面也很簡單。直接呼叫ThreadPoolExecutor構造一個就可以了,也可以通過Executors靜態工廠構建,但一般不建議。

可以看到,開發者想要在程式碼中使用執行緒池還是比較簡單的,這得益於Java給我們封裝好的一系列API。但是,這些API的背後是什麼呢,讓我們來揭開這個迷霧,看清執行緒池的本質。

執行緒池建構函式

通常,一般建構函式會反映出這個工具或這個物件的資料儲存結構。

如果把執行緒池比作一個公司。公司會有正式員工處理正常業務,如果工作量大的話,會僱傭外包人員來工作。

閒時就可以釋放外包人員以減少公司管理開銷。一個公司因為成本關係,僱傭的人員始終是有最大數。

如果這時候還有任務處理不過來,就走需求池排任務。

  • acc : 獲取呼叫上下文

  • corePoolSize: 核心執行緒數量,可以類比正式員工數量,常駐執行緒數量。

  • maximumPoolSize: 最大的執行緒數量,公司最多僱傭員工數量。常駐+臨時執行緒數量。

  • workQueue:多餘任務等待佇列,再多的人都處理不過來了,需要等著,在這個地方等。

  • keepAliveTime:非核心執行緒空閒時間,就是外包人員等了多久,如果還沒有活幹,解僱了。

  • threadFactory: 建立執行緒的工廠,在這個地方可以統一處理建立的執行緒的屬性。每個公司對員工的要求不一樣,恩,在這裡設定員工的屬性。

  • handler:執行緒池拒絕策略,什麼意思呢?就是當任務實在是太多,人也不夠,需求池也排滿了,還有任務咋辦?預設是不處理,丟擲異常告訴任務提交者,我這忙不過來了。

新增一個任務

接著,我們看一下執行緒池中比較重要的execute方法,該方法用於向執行緒池中新增一個任務。

核心模組用紅框標記了。

  • 第一個紅框:workerCountOf方法根據ctl的低29位,得到執行緒池的當前執行緒數,如果執行緒數小於corePoolSize,則執行addWorker方法建立新的執行緒執行任務;

  • 第二個紅框:判斷執行緒池是否在執行,如果在,任務佇列是否允許插入,插入成功再次驗證執行緒池是否執行,如果不在執行,移除插入的任務,然後丟擲拒絕策略。如果在執行,沒有執行緒了,就啟用一個執行緒。

  • 第三個紅框:如果新增非核心執行緒失敗,就直接拒絕了。

這裡邏輯稍微有點複雜,畫了個流程圖僅供參考

接下來,我們看看如何新增一個工作執行緒的?

新增worker執行緒

從方法execute的實現可以看出:addWorker主要負責建立新的執行緒並執行任務,程式碼如下(這裡程式碼有點長,沒關係,也是分塊的,總共有5個關鍵的程式碼塊):

  • 第一個紅框:做是否能夠新增工作執行緒條件過濾:

    • 判斷執行緒池的狀態,如果執行緒池的狀態值大於或等SHUTDOWN,則不處理提交的任務,直接返回;

  • 第二個紅框:做自旋,更新建立執行緒數量:

    • 通過引數core判斷當前需要建立的執行緒是否為核心執行緒,如果core為true,且當前執行緒數小於corePoolSize,則跳出迴圈,開始建立新的執行緒

有人或許會疑問 retry 是什麼?這個是java中的goto語法。只能運用在break和continue後面。

接著看後面的程式碼:

  • 第一個紅框:獲取執行緒池主鎖。

    • 執行緒池的工作執行緒通過Woker類實現,通過ReentrantLock鎖保證執行緒安全。

  • 第二個紅框:新增執行緒到workers中(執行緒池中)。

  • 第三個紅框:啟動新建的執行緒。

接下來,我們看看workers是什麼。

一個hashSet。所以,執行緒池底層的儲存結構其實就是一個HashSet。

worker執行緒處理佇列任務

  • 第一個紅框:是否是第一次執行任務,或者從佇列中可以獲取到任務。

  • 第二個紅框:獲取到任務後,執行任務開始前操作鉤子。

  • 第三個紅框:執行任務。

  • 第四個紅框:執行任務後鉤子。

這兩個鉤子(beforeExecute,afterExecute)允許我們自己繼承執行緒池,做任務執行前後處理。

到這裡,原始碼分析到此為止。接下來做一下簡單的總結。

總結

所謂執行緒池本質是一個hashSet。多餘的任務會放在阻塞佇列中。

只有當阻塞佇列滿了後,才會觸發非核心執行緒的建立。所以非核心執行緒只是臨時過來打雜的。直到空閒了,然後自己關閉了。

執行緒池提供了兩個鉤子(beforeExecute,afterExecute)給我們,我們繼承執行緒池,在執行任務前後做一些事情。

執行緒池原理關鍵技術:鎖(lock,cas)、阻塞佇列、hashSet(資源池)

最後希望對你理解執行緒池有幫助。最後,留一個思考題,為什麼執行緒池的底層資料介面採用HashSet來實現?

鍵值對資料結構,執行緒安全,去重