承上啓下:上一篇文章小豹子講了我爲何想要研究線程池的代碼,以及我計劃要怎樣閱讀代碼。這篇文章我主要閱讀了線程池實例化相關的代碼,並提出了本身的疑問。java
咱們首先看構造器的聲明,ThreadPoolExecutor
有四個重載構造器,其中三個分別指定了不一樣的缺省參數值,咱們直接看參數最全的構造器:數據庫
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 複製代碼
參數有點多,咱們有點懵,但並非無從下手。咱們去看代碼上方的 JavaDoc:服務器
allowCoreThreadTimeOut
corePoolSize
)時,大於核心池數量部分的線程空閒持續 keepAliveTime
時間後,將被終止keepAliveTime
參數的時間單位execute
方法提交的 Runnable
任務executor
建立新線程時使用的線程工廠那麼咱們根據文檔來建立一個線程池:併發
@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
咱們跟蹤到代碼中,看實例化的過程當中,構造器爲咱們作了什麼:函數
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;
複製代碼
又是一堆天書,但彷佛 RUNNING
、SHUTDOWN
等是表示某種狀態的常量,至於它們的賦值爲何這麼特殊,其餘變(常)量都是幹嗎的?老套路,看文檔。性能
文檔告訴咱們:ctl
是表示線程池狀態的原子整形,它包含兩部分:工做線程數、運行狀態。爲了將兩個變量用一個原子整形表示,咱們限制工做線程數最多隻能有 (2^29)-1
(大概 5 億)個,而空餘的高三位用來存儲運行狀態。學習
運行狀態可能有這些值:
TIDYING
時將調用 terminated()
回調方法terminated()
方法完成後這些值之間的順序很重要,運行狀態的值隨時間單調遞增,但在一個生命週期內不須要經歷過全部的狀態。
狀態的轉換:
shutdown()
觸發,或者隱含在 finalize()
中shutdownNow()
觸發terminated()
執行結束以後看過文檔以後,咱們再回頭看這幾個常量的賦值:首先 COUNT_BITS
是 Integer
的長度減 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 的線程池狀態。
那麼 runStateOf
與 workerCountOf
就沒必要多說,是從 ctl
中剝離出運行狀態值和線程數,在這裏 CAPACITY
的做用就體現出來,它表示一種標誌位,由於它二進制值的特性(前文提到)使得另外一個值與它進行位與(或非與)運算時能夠獲得值的低位(或高位)。接下來我着重解釋一下 isRunning(int c)
,首先咱們要已知兩個事實:
RUNNING
是最小的,SHUTDOWN
次之。ctl
變量的高三位。那麼判斷當前線程池的狀態是否爲 RUNNING
,有沒有必要將 ctl
中的狀態值提取出來,再與 RUNNING
常量進行對比呢?沒有必要,由於狀態值佔高位,只要狀態值小於 SHUTDOWN
,ctl
就必然小於 SHUTDOWN
,而小於 SHUTDOWN
的狀態只有 RUNNING
,所以只要 ctl
值小於 SHUTDOWN
,它就必定是 RUNNING
狀態。其餘函數(runStateLessThan
、runStateAtLeast
)同理,直接對比就好。
看到 ThreadPoolExecutor
中用一個原子變量存儲兩種狀態的設計思想,我心中產生一個疑問:爲何要這樣作?爲了節省內存麼?確定不是,線程池的主要應用場景應該是服務器,而用時間換空間(還只換了這麼點空間)是很是不值得的。那麼我惟一能想到的解釋是,有利於提升併發性能。
我記得我在看《高性能 MySQL》的時候,做者告訴我這樣一種思想:熱點分離。
書中描繪了這樣一個應用場景,一個相似微博的應用,後臺要統計總髮貼數。那麼每一次獲取數據都要 count(*)
這確定不現實。現實一點的作法是,在數據庫中維護一個表示總髮貼數的記錄,每一次用戶發帖,這個值就加 1。這種方案併發性能也不是很好。由於這個字段至少要加行鎖,每次用戶發帖,總髮貼數加 1 時都會引發鎖競爭。這至關於把用戶發帖行爲串行化了。
書中的解決方案是設計一張表,其中有 n 條記錄(好比說 100 條),每一次用戶發帖,在這 100 條記錄中選一條記錄(能夠是隨機選擇,也能夠根據時間取模)自加 1。而後每隔一段時間將表中的全部記錄加和賦值到第一條記錄中,刪除其餘記錄。這樣一來,原先是 N 個線程爭搶一把鎖,如今是 N 個線程爭搶一百把鎖。併發性能固然獲得了增長。這就是所謂的熱點分離。
但 ThreadPoolExecutor
中 ctl
的設計彷佛反其道而行之。把兩個須要併發訪問的值「捏」到了一塊兒。除非運行狀態和線程數每每同時變化,不然這樣作,我理解不了它是怎樣提升併發性能的。我決定暫時擱置這個問題,在後續對源碼的學習過程當中,我相信我能獲得答案。
小豹子仍是一個大三的學生,小豹子但願你能「批判性的」閱讀本文,對本文內容中不正確、不穩當之處進行嚴厲的批評,小豹子感激涕零。