小豹子帶你看源碼:Java 線程池(二)實例化

承上啓下:上一篇文章小豹子講了我爲何想要研究線程池的代碼,以及我計劃要怎樣閱讀代碼。這篇文章我主要閱讀了線程池實例化相關的代碼,並提出了本身的疑問。java

3 千里之行,始於實例化

3.1 先建立一個線程池玩玩

咱們首先看構造器的聲明,ThreadPoolExecutor 有四個重載構造器,其中三個分別指定了不一樣的缺省參數值,咱們直接看參數最全的構造器:數據庫

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 複製代碼

參數有點多,咱們有點懵,但並非無從下手。咱們去看代碼上方的 JavaDoc:服務器

  • corePoolSize:要保留在池中的線程數。即使線程空閒,不小於該參數的線程也將被保留。除非設置了 allowCoreThreadTimeOut
  • maximumPoolSize:池中容許的最大線程數
  • keepAliveTime:當池中線程數大於核心池數量(corePoolSize)時,大於核心池數量部分的線程空閒持續 keepAliveTime 時間後,將被終止
  • unit:keepAliveTime 參數的時間單位
  • workQueue:在任務被執行以前用於保存任務的隊列。這個隊列只包含由 execute 方法提交的 Runnable 任務
  • threadFactory:executor 建立新線程時使用的線程工廠
  • handler:用於處理因爲超過線程上限或隊列上限而產生的拒絕服務異常

那麼咱們根據文檔來建立一個線程池:併發

@Test
public void newInstanceTest() {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, 
        new LinkedBlockingQueue<Runnable>(), 
        new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread();
            }
        }, new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println("拒絕服務");
        }
    });
}
複製代碼

這裏咱們建立了一個核心池數量爲 5,最大線程數爲 10,線程保持時間爲 60 秒的線程池。ide

3.2 初始化時,線程池作了什麼?

咱們跟蹤到代碼中,看實例化的過程當中,構造器爲咱們作了什麼:函數

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
複製代碼

這裏很容易理解,前面進行了輸入參數的檢查,this.acc 是訪問控制器上下文,這裏咱們不深刻研究它。惟一值得一提的就是 unit.toNanos(keepAliveTime),這是將參數中的 keepAliveTime 轉換成納秒,彷佛也不難理解,但我有一個疑問:爲何要抽象時間單位?抽象時間段很差麼?好比我設計一個 Period 類表示一段時間,裏面有幾個靜態方法用於實例化,好比 Period.fromSeconds(long n) 表示 n 秒的一段時間,而後可使用 Period#toNanos() 這類的方法將該段時間傳化爲納秒。這樣能夠是參數更簡潔,表意更明確。不知兩種設計方案的優缺,還望各位指點。高併發

咱們繼續看 ThreadPoolExecutor 的初始化:post

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
複製代碼

又是一堆天書,但彷佛 RUNNINGSHUTDOWN 等是表示某種狀態的常量,至於它們的賦值爲何這麼特殊,其餘變(常)量都是幹嗎的?老套路,看文檔。性能

文檔告訴咱們:ctl 是表示線程池狀態的原子整形,它包含兩部分:工做線程數、運行狀態。爲了將兩個變量用一個原子整形表示,咱們限制工做線程數最多隻能有 (2^29)-1(大概 5 億)個,而空餘的高三位用來存儲運行狀態。學習

運行狀態可能有這些值:

  • RUNNING:容許提交新任務,處理隊列中的任務
  • SHUTDOWN:不容許提交新任務,但處理隊列中的任務
  • STOP:不容許提交新任務,不處理隊列中的任務,打斷執行中的任務
  • TIDYING:全部任務已經終止,工做線程數爲零,線程過渡到 TIDYING時將調用 terminated()回調方法
  • TERMINATED:terminated() 方法完成後

這些值之間的順序很重要,運行狀態的值隨時間單調遞增,但在一個生命週期內不須要經歷過全部的狀態。

狀態的轉換:

  • RUNNING -> SHUTDOWN:調用 shutdown() 觸發,或者隱含在 finalize()
  • (RUNNING / SHUTDOWN) -> STOP:調用 shutdownNow() 觸發
  • SHUTDOWN -> TIDYING:當隊列和池均爲空時觸發
  • STOP -> TIDYING:當池爲空時觸發
  • TIDYING -> TERMINATED:terminated() 執行結束以後

看過文檔以後,咱們再回頭看這幾個常量的賦值:首先 COUNT_BITSInteger 的長度減 3,其餘幾個狀態量分別是 -一、0、1,2,3 向高位移動 COUNT_BITS 位的結果,這也就對應着文檔所寫,用一個整形的高三位來存儲線程池的狀態。CAPACITY 的值是 1 向高位移動 COUNT_BITS 位再減一,字面意思是容量,這不難理解,COUNT_BITS 就是表明線程池所能容納的最大線程數,而值得一提的是,這個值在二進制層面上具備另外一個意義:CAPACITY 的二進制值高三位爲 0,其餘位爲 1。具體用途,咱們後面細說。

如今只剩 ctl 咱們不清楚了,首先從文檔中咱們能夠獲知 ctl 是包含了運行狀態與線程數量的一個整形原子變量,那麼 ctlOf(RUNNING, 0) 是什麼意思呢?咱們來看 ThreadPoolExecutor 中的靜態方法:

private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

private static boolean runStateLessThan(int c, int s) {
    return c < s;
}

private static boolean runStateAtLeast(int c, int s) {
    return c >= s;
}

private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}
複製代碼

這裏小豹子帶你們回憶一下位運算:
& 是按位與運算符,輸入均爲 1 輸出爲 1,其餘爲 0;
| 是按位或運算符,輸入均爲 0 輸出爲 0,其餘爲 1;
~ 是按位非運算符,輸入爲 0 輸出爲 1,輸入爲 1 輸出爲 0;

咱們看 ctlOf(int rs, int wc),其中 rs 指運行狀態(runState),wc 值線程數(workerCount)。rs 值的特色是高三位表示運行狀態,而其餘低位均爲 0,wc 值的特色是高三位爲 0(由於不大於 CAPACITY 嘛),低位表示線程數。那麼對兩個值進行按位或運算,正好就將兩個值的有效位合併到一個整形變量中。咱們再回頭看 ctl 變量的初始化 new AtomicInteger(ctlOf(RUNNING, 0))。這回應該就清楚了,ctlOf(RUNNING, 0) 表示運行狀態是 RUNNING,線程數爲 0 的線程池狀態。

那麼 runStateOfworkerCountOf 就沒必要多說,是從 ctl 中剝離出運行狀態值和線程數,在這裏 CAPACITY 的做用就體現出來,它表示一種標誌位,由於它二進制值的特性(前文提到)使得另外一個值與它進行位與(或非與)運算時能夠獲得值的低位(或高位)。接下來我着重解釋一下 isRunning(int c),首先咱們要已知兩個事實:

  1. 運行狀態值之間的順序很重要,運行狀態的值隨時間單調遞增,RUNNING 是最小的,SHUTDOWN 次之。
  2. 運行狀態儲存在 ctl 變量的高三位。

那麼判斷當前線程池的狀態是否爲 RUNNING,有沒有必要將 ctl 中的狀態值提取出來,再與 RUNNING 常量進行對比呢?沒有必要,由於狀態值佔高位,只要狀態值小於 SHUTDOWNctl 就必然小於 SHUTDOWN,而小於 SHUTDOWN 的狀態只有 RUNNING,所以只要 ctl 值小於 SHUTDOWN,它就必定是 RUNNING 狀態。其餘函數(runStateLessThanrunStateAtLeast)同理,直接對比就好。

3.3 疑問

看到 ThreadPoolExecutor 中用一個原子變量存儲兩種狀態的設計思想,我心中產生一個疑問:爲何要這樣作?爲了節省內存麼?確定不是,線程池的主要應用場景應該是服務器,而用時間換空間(還只換了這麼點空間)是很是不值得的。那麼我惟一能想到的解釋是,有利於提升併發性能。

我記得我在看《高性能 MySQL》的時候,做者告訴我這樣一種思想:熱點分離。

書中描繪了這樣一個應用場景,一個相似微博的應用,後臺要統計總髮貼數。那麼每一次獲取數據都要 count(*) 這確定不現實。現實一點的作法是,在數據庫中維護一個表示總髮貼數的記錄,每一次用戶發帖,這個值就加 1。這種方案併發性能也不是很好。由於這個字段至少要加行鎖,每次用戶發帖,總髮貼數加 1 時都會引發鎖競爭。這至關於把用戶發帖行爲串行化了。

書中的解決方案是設計一張表,其中有 n 條記錄(好比說 100 條),每一次用戶發帖,在這 100 條記錄中選一條記錄(能夠是隨機選擇,也能夠根據時間取模)自加 1。而後每隔一段時間將表中的全部記錄加和賦值到第一條記錄中,刪除其餘記錄。這樣一來,原先是 N 個線程爭搶一把鎖,如今是 N 個線程爭搶一百把鎖。併發性能固然獲得了增長。這就是所謂的熱點分離。

ThreadPoolExecutorctl 的設計彷佛反其道而行之。把兩個須要併發訪問的值「捏」到了一塊兒。除非運行狀態和線程數每每同時變化,不然這樣作,我理解不了它是怎樣提升併發性能的。我決定暫時擱置這個問題,在後續對源碼的學習過程當中,我相信我能獲得答案。

系列文章

小豹子仍是一個大三的學生,小豹子但願你能「批判性的」閱讀本文,對本文內容中不正確、不穩當之處進行嚴厲的批評,小豹子感激涕零。

相關文章
相關標籤/搜索