Java 線程池

線程池規範

《阿里巴巴 Java 開發手冊》對線程池的規範創建和使用做了定義。

由線程池提供線程

在這裏插入圖片描述
即不要按照以下方式創建線程:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // do something by a thread
    }
});

如果在 IntelliJ IDEA 中安裝了 Alibaba Java Coding Guidelines 插件,那麼顯示創建線程的語句會被提示:
在這裏插入圖片描述
提示內容如下:
不要顯式創建線程,請使用線程池。
線程資源必須通過線程池提供,不允許在應用中自行顯式創建線程。 說明:使用線程池的好處是減少在創建和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
    .setNameFormat("demo-pool-%d").build();
ExecutorService singleThreadPool = new ThreadPoolExecutor(1, 1,
    0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

singleThreadPool.execute(()-> System.out.println(Thread.currentThread().getName()));
singleThreadPool.shutdown();

使用 ThreadPoolExecutor 創建線程池

在這裏插入圖片描述
使用 ThreadPoolExecutor 創建線程池可以自定義各參數,比使用 Executors 更加靈活,而且通過合理設置參數可以避免出現 OOM。
如果在 IDEA 中使用 Executors 創建線程池,會被阿里的插件提示
在這裏插入圖片描述
提示內容如下:
手動創建線程池,效果會更好哦。
線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors 返回的線程池對象的弊端如下:
1)FixedThreadPoolSingleThreadPool
  允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2)CachedThreadPool
  允許的創建線程數量爲 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。

  • Positive example 1:

    //org.apache.commons.lang3.concurrent.BasicThreadFactory
    ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
        new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
    
  • Positive example 2:

    ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
            .setNameFormat("demo-pool-%d").build();
    
    //Common Thread Pool
    ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
    
    pool.execute(()-> System.out.println(Thread.currentThread().getName()));
    pool.shutdown();//gracefully shutdown
    
  • Positive example 3:

    <bean id="userThreadPool"
        class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="10" />
        <property name="maxPoolSize" value="100" />
        <property name="queueCapacity" value="2000" />
    
    <property name="threadFactory" value= threadFactory />
        <property name="rejectedExecutionHandler">
            <ref local="rejectedExecutionHandler" />
        </property>
    </bean>
    
    //in code
    userThreadPool.execute(thread);
    

線程池參數

ThreadPoolExecutor 構造方法

線程池的參數說明以 J.U.C(java.util.concurrent) 提供的 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;
}

參數簡介

各參數說明

序號 名稱 類型 含義
1 corePoolSize int 核心線程池大小
2 maximumPoolSize int 最大線程池大小
3 keepAliveTime long 線程最大空閒時間
4 unit TimeUnit 時間單位
5 workQueue BlockingQueue<Runnable> 線程等待隊列
6 threadFactory ThreadFactory 線程創建工廠
7 handler RejectedExecutionHandler 拒絕策略

參數之間的關係
在這裏插入圖片描述

參數詳解

受限於硬件、內存和性能,我們不可能無限制的創建任意數量的線程,因爲每一臺機器允許的最大線程是一個有界值。也就是說 ThreadPoolExecutor 管理的線程數量是有界的。線程池就是用這些有限個數的線程,去執行提交的任務。然而對於多用戶、高併發的應用來說,提交的任務數量非常巨大,一定會比允許的最大線程數多很多。爲了解決這個問題,必須要引入排隊機制,或者是在內存中,或者是在硬盤等容量很大的存儲介質中。 ThreadPoolExecutor 只支持任務在內存中排隊,通過 BlockingQueue 暫存還沒有來得及執行的任務。
任務的管理是一件比較容易的事,複雜的是線程的管理,這會涉及線程數量、等待/喚醒、同步/鎖、線程創建和死亡等問題。ThreadPoolExecutor 與線程相關的幾個成員變量是:keepAliveTimeallowCoreThreadTimeOutpoolSizecorePoolSizemaximumPoolSize,它們共同負責線程的創建和銷燬。

corePoolSize 核心線程池大小

線程池的基本大小,即在沒有任務需要執行的時候線程池的大小,並且只有在工作隊列滿了的情況下才會創建超出這個數量的線程。這裏需要注意的是:在剛剛創建 ThreadPoolExecutor 的時候,線程並不會立即啓動,而是要等到有任務提交時纔會啓動,除非調用了 prestartCoreThread / prestartAllCoreThreads 事先啓動核心線程。再考慮到 keepAliveTimeallowCoreThreadTimeOut 超時參數的影響,所以沒有任務需要執行的時候,線程池的大小不一定是 corePoolSize

maximumPoolSize 最大線程池大小

線程池中允許的最大線程數,線程池中的當前線程數目不會超過該值。如果隊列中任務已滿,並且當前線程個數小於 maximumPoolSize,那麼會創建新的線程來執行任務。這裏值得一提的是 largestPoolSize,該變量記錄了線程池在整個生命週期中曾經出現的最大線程個數。爲什麼說是曾經呢?因爲線程池創建之後,可以調用 setMaximumPoolSize() 改變運行的最大線程的數目。

poolSize 當前線程池大小

線程池中當前線程的數量,當該值爲0的時候,意味着沒有任何線程,線程池會終止;同一時刻,poolSize 不會超過 maximumPoolSize

  • keepAliveTime 線程最大空閒時間
    • keepAliveTime 的單位是納秒,1s=1000000000ns,1秒等於10億納秒。
    • keepAliveTime 是線程池中空閒線程等待工作的超時時間。
    • 當線程池中線程數量大於 corePoolSize(核心線程數量)或設置了 allowCoreThreadTimeOut(是否允許空閒核心線程超時)時,線程會根據 keepAliveTime 的值進行活性檢查,一旦超時便銷燬線程。否則,線程會永遠等待新的工作。

workQueue 任務隊列

用於保存等待執行的任務的阻塞隊列。可以選擇以下幾個阻塞隊列。

  • ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
  • LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按 FIFO (先進先出) 排序元素,吞吐量通常要高於 ArrayBlockingQueue。靜態工廠方法 Executors.newFixedThreadPool() 使用了這個隊列
  • SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於 LinkedBlockingQueue,靜態工廠方法 Executors.newCachedThreadPool()使用了這個隊列。
  • PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。

threadFactory 線程工廠

如果這裏沒有傳參的話,會使用 Executors 中的默認線程工廠類 DefaultThreadFactory

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
public static ThreadFactory defaultThreadFactory() {
    return new 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;
    }
}

DefaultThreadFactory 的構造方法可知,此工廠創建的線程的前綴格式如下: pool-1-thread-1,第一個數字 1 代表線程池的序號,第二個數字 1 代表線程的序號,且創建的線程爲非守護線程(用戶線程),優先級爲 Thread.NORM_PRIORITY(值爲 5)。

但阿里的規範建議使用第三方的線程工廠,設置有意義的線程名:

ExecutorService THREAD_POOL = new ThreadPoolExecutor(3, 5,
                1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(32), 
                new ThreadFactoryBuilder().setNameFormat("guava-pool-%d").build());

ThreadFactoryBuilder 是第三方庫 guava 中的一個線程工廠類,要使用 guava 需要引入其 Maven 依賴:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.2-jre</version>
</dependency>

handler 拒絕策略

拒絕任務:拒絕任務是指當線程池裏面的線程數量達到 maximumPoolSizeworkQueue 隊列已滿的情況下被嘗試添加進來的任務。
handler: 線程池對拒絕任務的處理策略。在 ThreadPoolExecutor 裏面定義了 4 種 handler 策略,分別是

  1. CallerRunsPolicy :這個策略重試添加當前的任務,會自動重複調用 execute() 方法,直到成功。
  2. AbortPolicy :對拒絕任務拋棄處理,並且拋出異常。
  3. DiscardPolicy :對拒絕任務直接無聲拋棄,沒有異常信息。
  4. DiscardOldestPolicy :對拒絕任務不拋棄,而是拋棄隊列裏面等待最久的一個線程,然後把拒絕任務加到隊列。

查看 ThreadPoolExecutor 中關於拒絕策略的源碼,可以發現以上四種策略都實現了 RejectedExecutionHandler 接口。

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();
        }
    }
}

/**
 * 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());
    }
}

/**
 * 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) {
    }
}

/**
 * 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);
        }
    }
}

查看 ThreadPoolExecutor 源碼,可以發現默認的 handlerAbortPolicy

/**
 * The default rejected execution handler
 */
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

參考博客

發佈了85 篇原創文章 · 獲贊 335 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章