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的原始碼
我們看下Executors
的newFixedThreadPool
和newSingleThreadExecutor
方法的原始碼:
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.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)
方法新增到執行緒池時:
-
如果此時執行緒池中執行緒的數量小於
corePoolSize
,即使執行緒池中的執行緒都處於空閒狀態,也要建立新的執行緒來處理被新增的任務。 -
如果此時執行緒池中的數量等於
corePoolSize
,但是緩衝佇列workQueue
未滿,那麼任務被放入緩衝佇列。 -
如果此時執行緒池中的數量大於
corePoolSize
,緩衝佇列workQueue
滿,並且執行緒池中的數量小於maximumPoolSize
,建新的執行緒來處理被新增的任務。 -
如果此時執行緒池中的數量大於
corePoolSize
,緩衝佇列workQueue
滿,並且執行緒池中的數量等於maximumPoolSize
,那麼通過 handler所指定的拒絕策略來處理此任務。
處理任務的優先順序為:核心執行緒數(corePoolSize) > 任務佇列容量(workQueue) > 最大執行緒數(maximumPoolSize);如果三者都滿了,使用rejectedExecutionHandler處理被拒絕的任務。
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),以及關於執行緒池的更多知識和實戰。