線程池工作機制與原理

書接上文,<a href="http://www.lxweimin.com/p/aa5884bcd032">Java線程池</a>。
接下來記錄一下線程池的工作機制和原理

線程池的兩個核心隊列:

  • 線程等待池,即線程隊列BlockingQueue。
  • 任務處理池(PoolWorker),即正在工作的Thread列表(HashSet<Worker>)。

線程池的核心參數:

  • 核心池大小(corePoolSize),即固定大小,設定好之后,線程池的穩定峰值,達到這個值之后池的線程數大小不會釋放。
  • 最大處理線程池數(maximumPoolSize),當線程池里面的線程數超過corePoolSize,小于maximumPoolSize時會動態創建與回收線程池里面的線程池資源。

線程池的運行機制:
舉個栗子。假如有一個工廠,工廠里面有10個人,每個工人同時只能做一件事情。因此只要當10個工人中有工人是空閑的,來了任務就分配給空閑的工人做;當10個工人都有任務時,如果還來任務,就把任務進行排隊等待。
如果說新任務數目增長的速度遠遠大于工作做任務的速度,那么此時工廠的主管可能就需要采取補救措施了,比如重新招4個工人進來;然后就將任務分配給這4個剛招進來的工人處理。
如果說這14個工人做任務的速度還是不夠,此時工廠主管就要考慮不再接受新的任務或者拋棄前面的一些任務了。當這14個工人當中有人空閑時,而新任務增長的速度又比較緩慢,工廠主管就要考慮辭掉4個臨時工了,只保持原來10個工人,比較額外的工人是需要花費的。
而這個栗子中永遠等待干活的10個工人機制就是workerQueue。這個栗子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。也就是說corePoolSize就是線程池的大小,maximumPoolSize在我看來就是一種線程池任務超過負荷的一種補救措施,即任務量突然過大時的一種補救措施。再看看下面圖好好理解一下。工人永遠在等待干活,就像workerQueue永遠在循環干活一樣,除非,整個線程池停止了。

線程池原理圖

線程池里面的線程的時序圖如下圖所示:

線程的時序圖

自定義線程池與ExecutorService

自定義線程池需要用到ThreadFactory,本節將通過創建一個線程的例子對ExecutorService及其參數進行詳細講解。

1.認識ExecutorService家族

ExecutorService家族成員如下所示:

ExecutorService家族

使用startUML畫的,我是UML菜鳥,所以湊合著看下。

上圖中主要元素說明如下:
Executor:線程池的頂級接口,但是嚴格意義上講Executor并不是一個線程池,而只是一個執行線程的工具。
ExecutorService:真正線程池接口。這個接口繼承了Executor接口,并聲明了一些方法:
submit、invokeAll、invokeAny以及shutDown等。
AbstractExecutorService實現了ExecutorService接口,基本實現了ExecutorService中聲明的所有方法。
ThreadPoolExecutor:ExecutorService的默認實現,繼承了類AbstractExecutorService。
ScheduledExecutorService:與Timer/TimerTask類似,解決那些需要任務重復執行的問題。
ScheduledThreadPoolExecutor:繼承ThreadPoolExecutor的ScheduledExecutorService接口實現,周期性任務調度的類實現。
Executors是個線程工廠類,方便我們快速地創建線程池。

2.利用ThreadFactory創建一個線程

java.util.concurrent.ThreadFactory提供了一個創建線程的工廠的接口。
ThreadFactory源碼如下:

public interface ThreadFactory{
  @override
  public Thread newThread(Runnable r);
}

我們可以看到上面的接口類中有一個newThread()的方法,為此我們自己手動定義一個線程工廠類,有木有激動啊,呵呵,下面我們就手動寫一個自己的線程工廠類吧!
MyThreadFactory.java

public class MyThreadFactory implements ThreadFactory{
  @Override
  public Thread newThread(Runnable r){
        return new Thread(r);
  }
}

上面已經創建好了我們自己的線程工廠類,但是啥都沒有做,就是直接new了一個Thread就返回回去了,我們一般在創建線程的時候,都需要定義其線程的名字,因為我們在定義了線程的名字之后就能在出現問題的時候根據監視工具來查找錯誤的來源,所以我們來看下官方實現的ThreadFactory吧!
這個類在java.util.concurrent.Executors類中的靜態類中DefaultThreadFactory

/**
*  The default thread factory
*/
static class DefaultThreadFactory implements ThreadFactory{
  private static final AtomicInteger poolNumber=new AtomicInteger(1);
  private final ThreadGroup group;
  private final AtomicInteger threadNumber=new AtomicInteger(1);
  private final String namePrefix;

  DefaultThreadFactory(){
    SecurityManager s=System.getSecurityManager();
    group=(s!=null)?s.getThreadGroup():Thread.currentThread().getThreadGroup();
    namePrefix="pool-"+poolNumber.getAndIncrement()+"-thread-";
  }
  public Thread newThread(Runnable r){
      Thread t=new Thread(group,r,namePrefix+threadNumber.getAndIncrement(),0);
      if((t.isDaemon())
          t.setDaemon(false);
      if(t.getPriority()!=Thread.NORM_PRIORITY)
          t.setPriority(Thread.NORM_PRIORITY);
      return t;
  }
}

3.了解線程池的拒絕策略(RejectExecutionHandler)

當調用ThreadPoolExecutor的execute方法時,而此時線程池處于一個飽和的狀態,并且任務隊列也已經滿了那么就需要做丟棄處理,RejectExecutionHandler就是這樣的一個處理接口類。
RejectExecutionHandler.java

public interface RejectedExecutionHandler {

    /**
     * Method that may be invoked by a {@link ThreadPoolExecutor} when
     * {@link ThreadPoolExecutor#execute execute} cannot accept a
     * task.  This may occur when no more threads or queue slots are
     * available because their bounds would be exceeded, or upon
     * shutdown of the Executor.
     *
     * <p>In the absence of other alternatives, the method may throw
     * an unchecked {@link RejectedExecutionException}, which will be
     * propagated to the caller of {@code execute}.
     *
     * @param r the runnable task requested to be executed
     * @param executor the executor attempting to execute this task
     * @throws RejectedExecutionException if there is no remedy
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

在JDK里面有4中拒絕策略,如下圖所示:

線程池拒絕策略
  • AbortPolicy:一言不合就拋異常(默認使用策略)。
  • CallerRunsPolicy:只用調用者所在線程來運行任務。
  • DiscardOldestPolicy:丟棄隊列里最近的一個任務,并執行當前任務。
  • DiscardPolicy:不處理,直接丟棄。

來看下源碼吧:
AbortPolicy : 一言不合就拋異常的

   /**
     * A handler for rejected tasks that throws a
     * {@code RejectedExecutionException}.
     */
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always.
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

CallerRunsPolicy:調用者所在線程來運行任務

    /**
     * A handler for rejected tasks that runs the rejected task
     * directly in the calling thread of the {@code execute} method,
     * unless the executor has been shut down, in which case the task
     * is discarded.
     */
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

DiscardOldestPolicy :丟棄隊列里面最近的一個任務,并執行當前任務

    /**
     * A handler for rejected tasks that discards the oldest unhandled
     * request and then retries {@code execute}, unless the executor
     * is shut down, in which case the task is discarded.
     */
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

DiscardPolicy : 不處理,直接丟棄

/**
     * A handler for rejected tasks that silently discards the
     * rejected task.
     */
    public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

思考問題:
為什么有任務拒絕的情況發生呢:
這里先假設有一個前提:線程池里面有一個任務隊列,用于緩存所有待處理的任務,正在處理的任務將從任務隊列中移除。因此,在任務隊列長度有限的情況下,就會出現現任務的拒絕情況,需要一種策略來處理發生這種已滿無法加入的情況。另外,在線程池關閉的時候,也需要對任務加入隊列操作進行額外的協調處理。

4.ThreadPoolExecutor詳解

ThreadPoolExecutor類是線程池中最核心的一個類,因此如果要想透徹的了解Java線程池,必須先了解這個大BOSS,下面來看下其源碼:
4種構造方法:

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), defaultHandler);
    }

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,threadFactory, defaultHandler);
    }

    public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,
                              BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
    }

    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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

通過源碼我們清楚的看到,最終構造函數調用了最后一個構造函數,后面的那個構造函數才是真正的構造函數,接下來研究一下參數。

  • int corePoolSize:核心池大小,這個參數跟后面講的線程池原理有很大的關系。在創建了線程池之后,默認情況下,線程池中并沒有任何線程,而是等待所有的任務到來之時才進行創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法 ,從這個兩個方法的名字可以知道是預創建線程的意思,即在沒有任務來臨之前先創建好corePoolSize個線程或者一個線程。默認情況下,在創建好線程池之后,線程池中的線程數為0,當有任務來之后,就會創建一個線程去執行任務,當線程池中的線程數量達到corePoolSize后,就會把達到的任務放到緩存隊列中去。
  • int maximumPoolSize:線程池最大線程數量,這是個非常重要的參數,它表示在線程池中最多能創建線程的數量;在corePoolSize和maximumPoolSize的線程數會被自動釋放,而小于corePoolSize的則不會。
  • long keepAliveTime:表示線程沒有執行任務時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大于corePoolSize時,keepAliveTime才會生效,直到線程池數量不大于corePoolSize,即只有當線程池數量大于corePoolSize數量,超出這個數量的線程一旦到達keepAliveTime就會終止。但是如果調用了allowCoreThreadTimeout(boolean)方法,即使線程池的線程數量不大于corePoolSize,線程也會在keepAliveTime之后就終止,知道線程池的數量為0為止。
  • TimeUnit unit:參數keepAliveTime的時間單位,一個時間單位枚舉類。
  • BlockingQueue workQueue:一個阻塞隊列,用來存儲等待執行任務的隊列,這個參數選擇也很重要,會對線程池的運行過程產生重大影響,一般來說,這里的阻塞隊列就是(ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue;)。
  • ThreadFactory ThreadFactory:線程工廠,主要用來創建線程;可以是一個自定義的線程工廠,默認就是Executors.defaultThreadFactory()。用來在線程池中創建線程。
  • RejectedExecutionHandler handler:表示當拒絕處理任務時的策略,也是可以自定義的,默認是我們前面的4種取值:
  • ThreadPoolExecutor.AbortPolicy(默認的,一言不合即拋異常的)
  • ThreadPoolExecutor.DiscardPolicy(一言不合就丟棄任務)
  • ThreadPoolExecutor.DiscardOldestPolicy(一言不合就把最近的任務給拋棄,然后執行當前任務)
  • ThreadPoolExecutor.CallerRunsPolicy(由調用者所在線程來執行任務)

所以想自定義線程池就可以從上面的幾個參數入手。接下來具體看下代碼,了解一下實現原理:

   // 默認異常處理機制
   private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
   //任務緩存隊列,用來存放等待執行的任務
   private final BlockingQueue<Runnable> workQueue;
   //線程池的主要狀態鎖,對線程狀態(比如線程大小、runState等)的改變都需要這個鎖
   private final ReentrantLock mainLock = new ReentrantLock();
   //用來存放工作集
   private final HashSet<Worker> workers = new HashSet<Worker>();
   //volatile 可變變量關鍵字,寫的時候用mainLock做鎖,讀的時候無鎖,高性能
   private volatile long keepAliveTime;
   //是否允許核心線程超時
   private volatile boolean allowCoreThreadTimeOut;
   //核心線程數量
   private volatile int corePoolSize;
   //線程最大線程數量
   private volatile int maximumPoolSize;
   //任務拒絕策略
   private volatile RejectedExcutionHandler handler;

結合之前的知識,大概就能猜出里面是怎么實現的了,具體可以參考一下JDK的源代碼,這樣我們就能做到了解原理又會用了。

5.自定義實現一個簡單的Web請求連接池

我們來自定義一個簡單的Web請求線程池。模仿Web服務的需求場景說明如下:

  • 服務器可容納的最小請求數是多少。
  • 可以動態擴充的請求數大小是多少。
  • 多久回收多余線程數即請求數。
  • 用戶訪問量打了怎么處理。
  • 線程隊列機制采取有優先級的排隊的執行機制。
    根據上面的場景,看下這個線程池如何編寫?
public class MyExecutors extends Executors{
    //利用默認線程工廠和PriorityBlockingQueue隊列機制,當然了,我們還可以自定義ThreadFactory和繼承queue進行自定義擴展
   public static ExecutorService newMyWebThreadPool(int minSpareThreads,int maxThreads,int maxIdleTime){
    return new ThreadPoolExecutor(minSpareThread,maxThreads,maxIdleTime,TimeUnit.MILLISECONDS,
          new PriorityBlockingQueue<Runnable>());
  }
}

6.線程池在工作中的錯誤使用

  • (1)分不清楚線程是單例還是多對象。
  • (2)線程池數量設置很大。
  • (3)注意死鎖問題
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容