1 线程池前置知识
1.1 Callable
以往当我们创建一个线程时,需要实现Runnable接口来创建一个任务,并交由Thread对象去执行。在JDK1.5之后,还提供了另外一种方式:实现Callable接口来创建任务。Callable与Runnable类似,都可以用来创建一个可被Thread对象执行的任务。它们的不同之处在于,Runnable的run()方法并没有返回值,而Callable的call()方法有返回值,可以在任务执行完之后返回结果。
1.2 Future
Future用来存放任务执行的将来才会产生的结果。其用法如下:
public class TestThreadPool {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<String> task = new Callable() {
@Override
public String call() throws Exception {
// do something
return "testCallable";
}
};
ExecutorService executor = Executors.newCachedThreadPool();
Future future = executor.submit(task);
System.out.println(future.get());
}
}
利用Callable创建一个任务,调用submit(task)方法提交任务之后,立即返回。而任务的执行结果将保存在Future中,通过Future.get()方法可以获取到任务的执行结果。需要注意的是,get()方法会阻塞等待任务返回执行结果,只有当获取到执行结果之后,该方法才会继续往下执行。
1.3 FutureTask
从上图可以看出,FutureTask相当于Runnable和Future的组合,这意味着FutureTask既是一个可以被Thread执行的任务,又可以保存任务执行的结果。
2 ThreadPoolExecutor
2.1 ThreadPoolExecutor继承体系
如上图所示,线程池ThreadPoolExecutor的顶层是一个Executor接口,其中只生命了一个方法execute,这表明线程池其实就是一个可以用来执行任务的一个执行器。而ExecutorService接口中则声明了一些与线程池的生命周期相关的一些方法。ThreadPoolExecutor是ExecutorService的具体实现,使用这个类可以创建我们需要的线程池。
2.2 ThreadPoolExecutor参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// do something
}
上面是ThreadPoolExecutor的构造方法,创建一个ThreadPoolExecutor一共需要7个参数:
- corePoolSize:核心线程数。
- maximumPoolSize:最大线程数。
- keepAliveTime:工作线程空闲时的存活时间,当工作线程的空闲时间超过这个时间后,空闲工作线程将会被回收归还给操作系统,当然,核心线程会继续存活不参与回收。
- unit:工作线程空闲时的存活时间单位,可指定为秒、分钟、小时等。
- workQueue:工作队列,表示提交的任务将加入到哪个阻塞队列中。
- threadFactory:线程工厂,用于自定义线程的创建,可以设定线程名称等。
- handler:拒绝策略,当线程池中所有线程都处于工作中且等待队列已满的情况下,会根据拒绝策略来处理任务。
2.3 ThreadPoolExecutor任务提交流程
向线程池提交一个任务的流程如下:
- 先判断线程池是否运行,如果线程池没有运行,会根据拒绝策略拒绝任务。
- 如果线程池正在运行,判断当前线程数是否小于核心线程数,如果是,则创建新的工作线程并执行任务。
- 如果当前线程数大于等于核心线程数,判断等待队列是否已满,如果未满,则将任务加入到等待队列中,等待工作线程执行。
- 如果队列已满,判断当前线程数是否小于最大线程数,如果是,则创建新的工作线程并执行任务。
- 如果当前线程数等于最大线程数,则根据拒绝策略拒绝任务。
2.4 线程工厂
Executors提供了默认的线程工厂DefaultThreadFactory。但是在实际开发过程中,创建线程时,必须要为线程定义好有意义的线程名称,这样有助于出现异常时进行回溯。比如使用jstack导出线程栈的信息时,可以根据线程名称很方便地定位到抛出异常的代码。在创建线程池的时候,可以通过实现ThreadFactory接口来自定义创建线程的规则,在其中定义线程名称。
2.5 拒绝策略
JDK默认提供了4种拒绝策略:
- AbortPolicy:不执行任务且抛出异常。
- DiscardPolicy:不执行任务,并且不会抛出异常。
- DiscardOldestPolicy:将队列中最先提交的任务丢弃。
- CallerRunsPolicy:由提交任务的线程来执行任务。
在实际开发过程中,其实很少用到默认提供的拒绝策略。我们一般会通过实现RejectedExecutionHandler接口来自定义拒绝策略,比如将任务保存到数据库、放入消息队列进行持久化等,以便后续可以继续处理这些任务。
2.6 Executors提供的线程池
2.6.1 newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
该方法返回一个可以根据实际情况调整线程池中线程的数量的线程池。如果线程池中的线程数量过大,它可以有效的回收多余的线程,如果线程数不足,那么它可以创建新的线程。该线程池使用的工作队列是SynchronousQueue。
2.6.2 newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
该方法返回一个固定线程数量的线程池,该线程池中的线程数量始终不变,即不会再创建新的线程,也不会销毁已经创建好的线程,自始至终都是那几个固定的线程在工作,所以该线程池可以控制线程的最大并发数。该线程池使用的工作队列是LinkedBlockingQueue。
2.6.3 newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
该方法返回一个只有一个线程的线程池,即每次只能执行一个线程任务,多余的任务会保存到一个任务队列中,等待这一个线程空闲,当这个线程空闲了再按 FIFO 方式顺序执行任务队列中的任务,即保证了多个线程的执行顺序。该线程池使用的工作队列是LinkedBlockingQueue。
2.6.4 Executors总结
开发过程中,不建议使用Executors创建线程池,FixedThreadPool和SingleThreadExecutor允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM。CachedThreadPool允许创建的最大线程数为Integer.MAX_VALUE,可能会创建大量的线程,导致OOM。
2.7 ThreadPoolExecutor源码解析
2.7.1 Worker
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 使用了线程工厂创建了一个线程。传入的参数为当前worker
this.thread = getThreadFactory().newThread(this);
}
......
}
Worker实际上是对工作线程的封装,其内部的一些操作依赖于AQS。Worker的关键点在于,创建一个Worker时,使用线程工厂创建一个线程,传入的参数是Worker本身,因为Worker本身也实现了Runnable接口。所以必须要有新任务提交时,才有可能创建新的线程。
2.7.2 execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
// 1. worker数量比核心线程数小,直接创建worker执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 2. worker数量超过核心线程数,任务直接进入队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 3. 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务
else if (!addWorker(command, false))
reject(command);
}
execute方法主要有三个步骤:
- worker数量比核心线程数小,直接创建worker执行任务。
- worker数量超过核心线程数,任务直接进入队列。
- 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务。
可参考2.3章节进行分析。
2.7.3 addWorker
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// 先判断是否需要新增worker
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
// worker数量超过容量,直接返回false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 使用CAS的方式增加worker数量
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// 线程池状态发生变化,对外层循环进行自旋
if (runStateOf(c) != rs)
continue retry;
// 其他情况,直接内层循环进行自旋即可
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
// worker的添加必须是串行的,因此需要加锁
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// worker创建成功,启动worker线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// worker线程启动失败,说明线程池状态发生了变化(关闭操作被执行),需要进行shutdown相关操作
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
3 ForkJoinPool
如上图所示,ForkJoin提供一种将大任务拆分成小任务,并将小任务的结果汇总成大任务结果的一种机制。使用案例:
public class TestForkJoin {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 构造数据
int length = 100000000;
long[] arr = new long[length];
for (int i = 0; i < length; i++) {
arr[i] = ThreadLocalRandom.current().nextInt(Integer.MAX_VALUE);
}
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
// 提交任务
ForkJoinTask<Long> forkJoinTask = forkJoinPool.submit(new SumTask(arr, 0, arr.length));
// 获取结果
Long sum = forkJoinTask.get();
forkJoinPool.shutdown();
System.out.println("sum: " + sum);
System.out.println("fork join elapse: " + (System.currentTimeMillis() - start));
}
private static class SumTask extends RecursiveTask<Long> {
private long[] arr;
private int from;
private int to;
public SumTask(long[] arr, int from, int to) {
this.arr = arr;
this.from = from;
this.to = to;
}
@Override
protected Long compute() {
// 小于1000的时候直接相加,可灵活调整
if (to - from <= 1000) {
long sum = 0;
for (int i = from; i < to; i++) {
// 模拟耗时
sum += (arr[i]/3*3/3*3/3*3/3*3/3*3);
}
return sum;
}
// 分成两段任务
int middle = (from + to) / 2;
SumTask left = new SumTask(arr, from, middle);
SumTask right = new SumTask(arr, middle, to);
// 提交左边的任务
left.fork();
// 右边的任务直接利用当前线程计算,节约开销
Long rightResult = right.compute();
// 等待左边计算完毕
Long leftResult = left.join();
// 返回结果
return leftResult + rightResult;
}
}
}
总结
JDK提供了两种类型的线程池:ThreadPoolExecutor和ForkJoinPool。ForkJoinPool 不是为了替代 ExecutorService,而是它的补充,在某些应用场景下性能比 ExecutorService 更好,ForkJoinPool 主要是运用“分而治之”的思想。此外,ThreadPoolExecutor内部维护了一个工作线程容器,其中的workers从同一个工作队列中获取任务并执行;而ForkJoin中的每个工作线程会维护单独的等待队列。