Java併發 之 執行緒池系列 (2) 使用ThreadPoolExecutor構造執行緒池

西召發表於2019-04-01

Java併發 之 執行緒池系列 (2) 使用ThreadPoolExecutor構造執行緒池

Executors的“罪與罰”

在上一篇文章Java併發 之 執行緒池系列 (1) 讓多執行緒不再坑爹的執行緒池中,我們介紹了使用JDK concurrent包下的工廠和工具類Executors來建立執行緒池常用的幾種方法:

//建立固定執行緒數量的執行緒池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);

//建立一個執行緒池,該執行緒池會根據需要建立新的執行緒,但如果之前建立的執行緒可以使用,會重用之前建立的執行緒
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

//建立一個只有一個執行緒的執行緒池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
複製程式碼

誠然,這種建立執行緒池的方法非常簡單和方便。但仔細閱讀原始碼,卻把我嚇了一條: 這是要老子的命啊!

我們前面講過,如果有新的請求過來,線上程池中會建立新的執行緒處理這些任務,一直建立到執行緒池的最大容量(Max Size)為止;超出執行緒池的最大容量的Tasks,會被放入阻塞佇列(Blocking Queue)進行等待,知道有執行緒資源釋放出來為止;要知道的是,阻塞佇列也是有最大容量的,多餘佇列最大容量的請求不光沒有獲得執行的機會,連排隊的資格都沒有!

那這些連排隊的資格都沒有的Tasks怎麼處理呢?不要急,後面在介紹ThreadPoolExecutor的拒絕處理策略(Handler Policies for Rejected Task)的時候會詳細介紹。

說到這裡你也許有寫疑惑了,上面這些東西,我通常使用Executors的時候沒有指定過啊。是的,因為Executors很“聰明”地幫我們做了這些事情。

Executors的原始碼

我們看下ExecutorsnewFixedThreadPoolnewSingleThreadExecutor方法的原始碼:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
        nThreads, nThreads,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>());
}
複製程式碼
public static ExecutorService newSingleThreadExecutor() {
     return new FinalizableDelegatedExecutorService
         (new ThreadPoolExecutor(
         1, 1,
         0L, TimeUnit.MILLISECONDS,
         new LinkedBlockingQueue<Runnable>()));
 }
複製程式碼

其實它們底層還是通過ThreadPoolExecutor來建立ExecutorService的,這裡對它的引數先不作介紹,下面會詳細講,我們只說一下new LinkedBlockingQueue<Runnable>()這個引數。

LinkedBlockingQueue就是當任務數大於執行緒池的執行緒數的時候的阻塞佇列,這裡使用的是無參構造,我們再看一下建構函式:

/**
 * Creates a {@code LinkedBlockingQueue} with a capacity of
 * {@link Integer#MAX_VALUE}.
 */
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
複製程式碼

我們看到阻塞佇列的預設大小竟然是Integer.MAX_VALUE

如果不做控制,拼命地往阻塞佇列裡放Task,分分鐘“Out of Memory”啊!

還有更絕的,newCachedThreadPool方法:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
    0, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<Runnable>());
}
複製程式碼

最大執行緒數預設也是Integer.MAX_VALUE,也就是說,如果之前的任務沒有執行完就有新的任務進來了,就會繼續建立新的執行緒,直到建立到Integer.MAX_VALUE為止。

讓你的JVM OutOfMemoryError

下面提供一個使用newCachedThreadPool建立大量執行緒處理Tasks,最終OutOfMemoryError的例子。

友情提醒:場面過於血腥,請勿在生產環境使用。

package net.ijiangtao.tech.concurrent.jsd.threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample2 {

    private static final ExecutorService executorService = Executors.newCachedThreadPool();

    private static class Task implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(1000 * 600);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void newCachedThreadPoolTesterBadly() {
        System.out.println("begin............");
        for (int i = 0; i <= Integer.MAX_VALUE; i++) {
            executorService.execute(new Task());
        }
        System.out.println("end.");
    }

    public static void main(String[] args) {
        newCachedThreadPoolTesterBadly();
    }

}
複製程式碼

main方法啟動以後,開啟控制皮膚,看到CPU和記憶體幾乎已經全部耗盡:

Java併發 之 執行緒池系列 (2) 使用ThreadPoolExecutor構造執行緒池

很快控制檯就丟擲了java.lang.OutOfMemoryError

begin............
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:717)
	at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:957)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1378)
	at net.ijiangtao.tech.concurrent.jsd.threadpool.ThreadPoolExample2.newCachedThreadPoolTesterBadly(ThreadPoolExample2.java:24)
	at net.ijiangtao.tech.concurrent.jsd.threadpool.ThreadPoolExample2.main(ThreadPoolExample2.java:30)
複製程式碼

阿里巴巴Java開發手冊

下面我們在看Java開發手冊這條規定,應該就明白作者的良苦用心了吧。

【強制】執行緒池不允許使用Executors去建立,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。 說明: Executors返回的執行緒池物件的弊端如下: 1)FixedThreadPool和SingleThreadPool:允許的請求佇列長度為Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM。 2)CachedThreadPool和ScheduledThreadPool:允許的建立執行緒數量為Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致OOM。

主角出場

解鈴還須繫鈴人,其實避免這個OutOfMemoryError風險的鑰匙就藏在Executors的原始碼裡,那就是自己直接使用ThreadPoolExecutor

ThreadPoolExecutor的構造

構造一個ThreadPoolExecutor需要蠻多引數的。下面是ThreadPoolExecutor的建構函式。

/**
 * Creates a new {@code ThreadPoolExecutor} with the given initial
 * parameters.
 *
 * @param corePoolSize the number of threads to keep in the pool, even
 *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
 * @param maximumPoolSize the maximum number of threads to allow in the
 *        pool
 * @param keepAliveTime when the number of threads is greater than
 *        the core, this is the maximum time that excess idle threads
 *        will wait for new tasks before terminating.
 * @param unit the time unit for the {@code keepAliveTime} argument
 * @param workQueue the queue to use for holding tasks before they are
 *        executed.  This queue will hold only the {@code Runnable}
 *        tasks submitted by the {@code execute} method.
 * @param threadFactory the factory to use when the executor
 *        creates a new thread
 * @param handler the handler to use when execution is blocked
 *        because the thread bounds and queue capacities are reached
 * @throws IllegalArgumentException if one of the following holds:<br>
 *         {@code corePoolSize < 0}<br>
 *         {@code keepAliveTime < 0}<br>
 *         {@code maximumPoolSize <= 0}<br>
 *         {@code maximumPoolSize < corePoolSize}
 * @throws NullPointerException if {@code workQueue}
 *         or {@code threadFactory} or {@code handler} is null
 */
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;
}
複製程式碼

下面就一一介紹一下這些引數的具體含義。

ThreadPoolExecutor構造引數說明

其實從原始碼中的JavaDoc已經可以很清晰地明白這些引數的含義了,下面照顧懶得看英文的同學,再解釋一下:

  • corePoolSize

執行緒池核心執行緒數。

預設情況下核心執行緒會一直存活,即使處於閒置狀態也不會受存keepAliveTime限制,除非將allowCoreThreadTimeOut設定為true

  • maximumPoolSize

執行緒池所能容納的最大執行緒數。超過maximumPoolSize的執行緒將被阻塞。

最大執行緒數maximumPoolSize不能小於corePoolSize

  • keepAliveTime

非核心執行緒的閒置超時時間。

超過這個時間非核心執行緒就會被回收。

  • TimeUnit

keepAliveTime的時間單位,如TimeUnit.SECONDS。

當將allowCoreThreadTimeOut為true時,對corePoolSize生效。

  • workQueue

執行緒池中的任務佇列。

沒有獲得執行緒資源的任務將會被放入workQueue,等待執行緒資源被釋放。如果放入workQueue的任務數大於workQueue的容量,將由RejectedExecutionHandler的拒絕策略進行處理。

常用的有三種佇列: SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue

  • threadFactory

提供建立新執行緒功能的執行緒工廠。

ThreadFactory是一個介面,只有一個newThread方法:

Thread newThread(Runnable r);
複製程式碼
  • rejectedExecutionHandler

無法被執行緒池處理的任務的處理器。

一般是因為任務數超出了workQueue的容量。

當一個任務被加入執行緒池時

總結一下,當一個任務通過execute(Runnable)方法新增到執行緒池時:

  1. 如果此時執行緒池中執行緒的數量小於corePoolSize,即使執行緒池中的執行緒都處於空閒狀態,也要建立新的執行緒來處理被新增的任務。

  2. 如果此時執行緒池中的數量等於corePoolSize,但是緩衝佇列workQueue未滿,那麼任務被放入緩衝佇列。

  3. 如果此時執行緒池中的數量大於corePoolSize,緩衝佇列workQueue滿,並且執行緒池中的數量小於maximumPoolSize,建新的執行緒來處理被新增的任務。

  4. 如果此時執行緒池中的數量大於corePoolSize,緩衝佇列workQueue滿,並且執行緒池中的數量等於maximumPoolSize,那麼通過 handler所指定的拒絕策略來處理此任務。

處理任務的優先順序為:核心執行緒數(corePoolSize) > 任務佇列容量(workQueue) > 最大執行緒數(maximumPoolSize);如果三者都滿了,使用rejectedExecutionHandler處理被拒絕的任務。

Java併發 之 執行緒池系列 (2) 使用ThreadPoolExecutor構造執行緒池

ThreadPoolExecutor的使用

下面就通過一個簡單的例子,使用ThreadPoolExecutor構造的執行緒池執行任務。

ThreadPoolExample3

package net.ijiangtao.tech.concurrent.jsd.threadpool;

import java.time.LocalTime;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author ijiangtao.net
 */
public class ThreadPoolExample3 {

    private static final AtomicInteger threadNumber = new AtomicInteger(1);

    private static class Task implements Runnable {
        @Override
        public void run() {
            try {
                Thread.currentThread().sleep(2000);
                System.out.println(Thread.currentThread().getName() + "-" + LocalTime.now());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    private static class MyThreadFactory implements ThreadFactory {

        private final String namePrefix;

        public MyThreadFactory(String namePrefix) {
            this.namePrefix = namePrefix;
        }

        public Thread newThread(Runnable runnable) {
            return new Thread(runnable, namePrefix + "-" + threadNumber.getAndIncrement());
        }

    }

    private static final ExecutorService executorService = new ThreadPoolExecutor(
            10,
            20, 30, TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(50),
            new MyThreadFactory("MyThreadFromPool"),
            new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        // creates five tasks
        Task r1 = new Task();
        Task r2 = new Task();
        Task r3 = new Task();
        Task r4 = new Task();
        Task r5 = new Task();

        // submit方法有返回值
        Future future = executorService.submit(r1);
        System.out.println("r1 isDone ? " + future.isDone());

        // execute方法沒有返回值
        executorService.execute(r2);
        executorService.execute(r3);
        executorService.execute(r4);
        executorService.execute(r5);

        //關閉執行緒池
        executorService.shutdown();

    }

}
複製程式碼

執行結果

r1 isDone ? false
MyThreadFromPool-2-21:04:03.215
MyThreadFromPool-5-21:04:03.215
MyThreadFromPool-4-21:04:03.215
MyThreadFromPool-3-21:04:03.215
MyThreadFromPool-1-21:04:03.215
複製程式碼

從結果看,從執行緒池取出了5個執行緒,併發執行了5個任務。

總結

這一章我們介紹了一種更安全、更定製化的執行緒池構建方式:ThreadPoolExecutor。相信你以後不敢輕易使用Executors來構造執行緒池了。

後面我們會介紹執行緒池的更多實現方式(例如使用Google核心庫Guava),以及關於執行緒池的更多知識和實戰。

Links

作者資源

相關資源

相關文章