從實驗的角度理解執行緒池
今天正好複習到執行緒池,幾個引數看似簡單,但是越想越覺得有交差和不解。新建執行緒池的方法如下,分別是(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,handler),通過這幾個引數的"相互作用"來從新認識執行緒池的工作方式。
ThreadPoolExecutor executorService = new ThreadPoolExecutor( 1, 1, 1L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1), new ThreadPoolExecutor.DiscardPolicy());//這裡為了避免報錯,使用DiscardPolicy,預設的Reject是AbortPolicy在佇列已滿時會報錯
問題
首先我們提出這幾個問題
- 如果core > max 會怎麼樣?
- 如果core > Queue.length 會發生什麼?
- 如果max > Queue.length 執行緒池怎麼處理?
- 如果core = max = Queue.length 執行緒池如何處理?
- 正常情況下的賦值。
測試
測試程式碼如下
public static void main(String[] args) { ThreadPoolExecutor executorService = new ThreadPoolExecutor( 1, 1, 1L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1), new ThreadPoolExecutor.DiscardPolicy()); executorService.execute(new Runnable() { @Override public void run() { System.out.println("start runnable1" + Thread.currentThread()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("runnable 1 finish" + Thread.currentThread()); } }); for (int i = 2; i < 6; i++) { final int finalI = i; executorService.execute(new Runnable() { @Override public void run() { System.out.println("start runnable" + finalI + "" + Thread.currentThread()); } }); } executorService.shutdown(); System.out.println("main finish ------ " + Thread.currentThread()); }
大家最好自己跑一下,有一個直觀的認識,這裡用表格表達的也不甚明瞭,其實如果用動圖會好很多,不過自己太懶了=。=
-
執行緒佇列遞增
執行緒佇列依次增加
很明顯的可以看出,執行緒池中只有 1 個執行緒,執行緒依次執行(並且是線上程1結束之後才開始的),佇列越長能執行的執行緒越多,被捨棄的執行緒也就越少。
-
Max依次遞增
Max依次遞增
結論:Max的值規定了執行緒池中執行緒的上限,其實並不只是有一個Core執行緒就只能跑一個執行緒,當Queue裡面放不下的時候會開啟非核心執行緒來跑這個『意外』的任務,而且與核心執行緒無關(這些執行緒不像圖1中那樣等待了執行緒1)
-
Core > Max
Core>Max
報錯
-
Core依次遞增(Core不能大於Max,所以Max也增加了)
Core遞增
結論:都在核心程序裡面執行,和結論2類似
猜想
一直以來,我都是從字面上認為執行緒池的作用,Core即為能執行最多的執行緒量,Max就是超過Core需要排隊的那一部分,而Queue在我的臆想一直都是無限的。這就造成了一個非常狹隘的思維,對設計者的意圖沒有思考,對底層的程式碼沒有研究。
這裡大家可以好好想一下執行緒池的工作方式,Core、Max與Queue的相互關係(想清楚這個,其他幾個引數也能手到擒來)。
在這裡我們從上面的結論再次猜想一下,Core自然是核心執行緒的數量,當核心執行緒沒有滿時,無論執行緒1是否閒置,都會建立一個新的執行緒,並且這些執行緒可以重用(這裡就有一個存活時間的思考了);Max是執行緒池中可以存在的最大執行緒量,當超過Core執行緒數且小於Max時,這時執行緒池會新建一些執行緒來處理這些『來不及』處理的任務(來源於測試2),同時,Core不能大於Max(這是為什麼呢?);作為一個佇列,它擔任著『快取佇列』的任務,像普通的佇列一樣,最多能儲存多少就儲存多少。
驗證
當然是從原始碼角度
public void execute(Runnable command) { if (command == null)// 1 throw new NullPointerException(); int c = ctl.get(); if (workerCountOf(c) < corePoolSize) {// 2 if (addWorker(command, true)) return; c = ctl.get(); } if (isRunning(c) && workQueue.offer(command)) {// 3 int recheck = ctl.get(); if (! isRunning(recheck) && remove(command))// 4 reject(command); else if (workerCountOf(recheck) == 0)// 5 addWorker(null, false); } else if (!addWorker(command, false)) { reject(command);// 6 } }
首先,我覺得在看原始碼時應該明確自己的目的,這樣在理解時更有針對性;其次,在閱讀時一定不要糾結某一個地方,有時候看的程式碼成百上千行,不可能把每一個地方都看懂,比如在execute方法中就有位運算的使用、連結串列的操作還有addWorker一個更復雜的方法,不是說深究沒有必要,而是去把這個方法當做某個變數去理解,我們開車是要前往目的地,而不是要了解車的構造。
列出幾個方法的作用,更利於理解
ctl.get() //凡事ctl有關的操作其實都是位運算的使用,這裡有興趣的可以去查一下,並不難。 //這裡我們只要知道,它像一個int一樣,1代表一個狀態,2代表另一個狀態。 workerCountOf(c);//這裡都是來取ctl儲存的狀態,就是字面意思上的,(Core執行緒)的執行數量 isRunning(c);//(執行緒)是否正在執行 addWorker(commad,boolean); //建立一個新執行緒(重要),boolean表示建立的是核心執行緒還是非核心執行緒 workQueue.offer(command)// 將執行緒加入到佇列中(其實就是連結串列操作) reject(command);// 執行拒絕策略
有了以上的準備,我們在理解時就比較容易了。
- 檢查新執行緒是否為空。
- 獲取執行緒池狀態c,如果正在執行的Core執行緒小於預定值則建立一個新執行緒執行。(即使有空閒執行緒,也會建立新的)
- 如果已經超過了Core執行緒數,檢查執行緒是否正在執行,同時加入執行緒佇列成功。(如測試1)
- 雙重檢查,如果執行緒恰好執行完畢了,要從阻塞佇列中移除該執行緒。
- 如果沒有核心執行緒執行,建立非核心執行緒執行該任務。(如測試2)
- 沒有加入到核心執行緒,建立非核心執行緒也失敗了(如測試1)執行拒絕策略。
這裡相信大家對執行緒池都有了一定的自己的理解了,有什麼問題歡迎提出一起討論進步。