天天看點

深入源碼分析 Java 線程池實作原理

深入源碼分析 Java 線程池實作原理

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

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

深入源碼分析 Java 線程池實作原理

池化技術

深入源碼分析 Java 線程池實作原理

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

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

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

線程池、連接配接池、記憶體池、對象池等。

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

深入源碼分析 Java 線程池實作原理

建立一個線程

深入源碼分析 Java 線程池實作原理

在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 線程池實作原理

線程池的簡單使用

深入源碼分析 Java 線程池實作原理

以下代碼,是在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的背後是什麼呢,讓我們來揭開這個迷霧,看清線程池的本質。

深入源碼分析 Java 線程池實作原理

線程池構造函數

深入源碼分析 Java 線程池實作原理

通常,一般構造函數會反映出這個工具或這個對象的資料存儲結構。

深入源碼分析 Java 線程池實作原理

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

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

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

  • acc : 擷取調用上下文
  • corePoolSize: 核心線程數量,可以類比正式員工數量,常駐線程數量。
  • maximumPoolSize: 最大的線程數量,公司最多雇傭員工數量。常駐+臨時線程數量。
  • workQueue:多餘任務等待隊列,再多的人都處理不過來了,需要等着,在這個地方等。
  • keepAliveTime:非核心線程空閑時間,就是外包人員等了多久,如果還沒有活幹,解雇了。
  • threadFactory: 建立線程的工廠,在這個地方可以統一處理建立的線程的屬性。每個公司對員工的要求不一樣,恩,在這裡設定員工的屬性。
  • handler:線程池拒絕政策,什麼意思呢?就是當任務實在是太多,人也不夠,需求池也排滿了,還有任務咋辦?預設是不處理,抛出異常告訴任務送出者,我這忙不過來了。
深入源碼分析 Java 線程池實作原理

添加一個任務

深入源碼分析 Java 線程池實作原理

接着,我們看一下線程池中比較重要的execute方法,該方法用于向線程池中添加一個任務。

深入源碼分析 Java 線程池實作原理

核心子產品用紅框标記了。

  • 第一個紅框:workerCountOf方法根據ctl的低29位,得到線程池的目前線程數,如果線程數小于corePoolSize,則執行addWorker方法建立新的線程執行任務;
  • 第二個紅框:判斷線程池是否在運作,如果在,任務隊列是否允許插入,插入成功再次驗證線程池是否運作,如果不在運作,移除插入的任務,然後抛出拒絕政策。如果在運作,沒有線程了,就啟用一個線程。
  • 第三個紅框:如果添加非核心線程失敗,就直接拒絕了。

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

深入源碼分析 Java 線程池實作原理

接下來,我們看看如何添加一個工作線程的?

深入源碼分析 Java 線程池實作原理

添加worker線程

深入源碼分析 Java 線程池實作原理

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

深入源碼分析 Java 線程池實作原理
  • 第一個紅框:做是否能夠添加工作線程條件過濾:
  • 判斷線程池的狀态,如果線程池的狀态值大于或等SHUTDOWN,則不處理送出的任務,直接傳回;
  • 第二個紅框:做自旋,更新建立線程數量:
  • 通過參數core判斷目前需要建立的線程是否為核心線程,如果core為true,且目前線程數小于corePoolSize,則跳出循環,開始建立新的線程
有人或許會疑問 retry 是什麼?這個是java中的goto文法。隻能運用在break和continue後面。

接着看後面的代碼:

深入源碼分析 Java 線程池實作原理
  • 第一個紅框:擷取線程池主鎖。
  • 線程池的工作線程通過Woker類實作,通過ReentrantLock鎖保證線程安全。
  • 第二個紅框:添加線程到workers中(線程池中)。
  • 第三個紅框:啟動建立的線程。

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

深入源碼分析 Java 線程池實作原理

一個hashSet。是以,線程池底層的存儲結構其實就是一個HashSet。

深入源碼分析 Java 線程池實作原理

worker線程處理隊列任務

深入源碼分析 Java 線程池實作原理
深入源碼分析 Java 線程池實作原理
  • 第一個紅框:是否是第一次執行任務,或者從隊列中可以擷取到任務。
  • 第二個紅框:擷取到任務後,執行任務開始前操作鈎子。
  • 第三個紅框:執行任務。
  • 第四個紅框:執行任務後鈎子。

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

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

深入源碼分析 Java 線程池實作原理

總結

深入源碼分析 Java 線程池實作原理

所謂線程池本質是一個hashSet。多餘的任務會放在阻塞隊列中。

隻有當阻塞隊列滿了後,才會觸發非核心線程的建立。是以非核心線程隻是臨時過來打雜的。直到空閑了,然後自己關閉了。

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

線程池原理關鍵技術:鎖(lock,cas)、阻塞隊列、hashSet(資源池)

深入源碼分析 Java 線程池實作原理

最後希望對你了解線程池有幫助。最後,留一個思考題,為什麼線程池的底層資料接口采用HashSet來實作?

OW,文章樣式通過 openwrite.cn 免費轉換得到~

END

長按二維碼,掃掃關注哦

也是一種認可