线程池
在Java中有两种线程池
- ThreadPoolExecutor
自定义线程池,可以定义线程数量,等待队列,拒绝策略等 - ForkJoinPool
Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
主要的区别
- 使用场景不同,ThreadPoolExecutor可以自定义任务,ForkJoinPool适合具有父子关系的任务,适合分治算法
- 使用方法不同,ThreadPoolExecutor需要自定义线程池大小,等待队列具体类型(ArrayQueue需要指定queue长度,LinkQueue长度则为int的最大值),ForkJoinPool线程数量默认是CPU的核心数,等待队列近乎无限,有造成OOM的风险
后面线程池以ThreadPoolExecutor为基数进行讨论。
Executor、Future、FutureTask
Executor
Executor接口,是线程池的根接口:
下面我们来看下ThreadPoolExecutor的类关系图:
从第一幅截图中我们可以看到Executor接口中定义了一个execute方法,看方法名也很好理解,用来执行task,同时返回值类型为void。
下面我们简单看下其子接口ExecutorService中又定义了哪些内容:
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit) throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
很好理解,定义了一些shutdown方法,以及执行单个task,多个task的方法。
我们以submit为例:
我们一定可以看到方法的入参是Callable,或者Runnable,其方法返回值是个Future,这都啥玩样儿?
不急,我们看源码。
首先我们看下Runnable:
public interface Runnable {
public abstract void run();
}
再看下Callable:
public interface Callable<V> {
V call() throws Exception;
}
Future
至此,我们就可以弄清楚了,Runnable执行run方法,没有返回值,Callable执行的是call方法,这个是有返回值的,也就是说通过call方法,我们就可以拿到线程执行task的结果了。放到线程池中,对应的返回值类型就是个Future,那这个Future又是个啥呢?
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
根据接口定义,我们可以很清楚的知道了,Future封装了thread的运行状态,以及thread运行后的返回值,通过get方法,就可以拿到线程的执行结果。但是,请注意,这个get方法是阻塞的,如果线程内部有死循环等问题,线程将会一直堵塞下去,风险系数非常高。
建议使用其子类CompletableFuture,JDK已经封装了很多方便的异步逻辑,具体使用线程池这部分就不多做介绍了,后面有机会再写。
FutureTask
public class FutureTask<V> implements RunnableFuture<V> {
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
private Callable<V> callable;
private Object outcome; // non-volatile, protected by state reads/writes
private volatile Thread runner;
private volatile WaitNode waiters;
.
.
.
}
摘抄部分,可以看到FutureTask即实现了接口RunnableFuture,又持有Callable对象,可以认为FutureTask同时拥有Runnable和Callable特性。
ThreadPoolExecutor
终于讲到ThreadPoolExecutor了,我们先看到pool的构造器,以参数最全的那个为例:
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;
}
corePoolSize:核心线程数,这部分的线程就算空闲,也不会归还给os
maximumPoolSize:最大线程数,当任务量太多,核心线程处理不过来的时候,会动态扩容到的最大线程数量
keepAliveTime:线程最大空闲时间,空闲超过该时间,该线程归还给os
unit:时间单位
workQueue:线程阻塞queue,用来缓存暂时处理不过来的任务
threadFactory:线程工厂,用来定义以什么样的方式创建线程
handler:拒绝策略,当任务超过最大线程数之后,对后续任务怎么做处理。jdk自带4种策略,建议自定义。
执行过程
假设我们现在已经创建出了一个线程池,核心线程数2,最大线程数4,任务队列大小2。现在有7个task需要被执行,过程如下:
- task1:此时线程池内没有线程,首先创建出一个线程thread1来执行task1
- task2:核心线程数为2,现有线程数为1,没有超过设定值,创建线程thread2来执行task2
- task3:核心线程数2,现有线程数2,已经达到设定值,且等待队列为空,将task3放入等待队列
- task4:与task3类似,等待队列没有满,将task4放入等待队列
- task5:核心线程数已满,等待队列已满,但是最大线程数为4,现有线程为2,则创建线程thread3执行task5
- task6:与task5类似,还没有达到最大线程数,创建thread4执行task6
- task7:核心线程已满,最大线程已满,等待队列已满,线程池无法消化该任务,执行拒绝策略。
实现原理
我们以submit为例,探讨下线程池工作原理:
首先是入口方法:
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
首先先将要执行的task封装为RunnableFuture对象ftask(实际是new了一个FutureTask),这个对象本身是Runnable接口的实例,所以可以直接将ftask扔给execute方法,执行task,同时将ftask返回(即实际返回类型是个FutureTask)。
下面再看下execute方法:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // 32-3 = 29
private static final int CAPACITY = (1 << COUNT_BITS) - 1; //最大线程数 2^29 -1
private static final int RUNNING = -1 << COUNT_BITS; //线程池运行状态
private static final int SHUTDOWN = 0 << COUNT_BITS; // 线程池关闭状态,不接受新任务,会处理完已经接受的任务
private static final int STOP = 1 << COUNT_BITS; // 立即关闭线程池,中断正在处理的任务
private static final int TIDYING = 2 << COUNT_BITS; // 所有任务已完成
private static final int TERMINATED = 3 << COUNT_BITS;
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
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);
}
else if (!addWorker(command, false))
reject(command);
}
从上面的源码中可以看出,线程池的状态以及数量,都跟COUNT_BITS这个变量有关系:
- COUNT_BITS:使用了int 32位的后29位,
- RUNNING: -1 << COUNT_BITS
-1在Java底层是由32个1表示的,左移29位的话,即111 00000 00000000 00000000 00000000,也就是低29位全部为0,高3位全部为1的话,表示RUNNING状态,即-536870912; - SHUTDOWN = 0 << COUNT_BITS
0在Java底层是由32个0表示的,无论左移多少位,还是32个0,即000 00000 00000000 00000000 00000000,也就是低29位全部为0,高3位全部为0的话,表示SHUTDOWN状态,即0; - STOP = 1 << COUNT_BITS
1在Java底层是由前面的31个0和1个1组成的,左移29位的话,即001 00000 00000000 00000000 00000000,也就是低29位全部为0,高3位为001的话,表示STOP状态,即536870912; - 后面还有两个也类似,就不写了
从上面的解释我们便可以看出,ctl的前3位表示的是当前线程池的运行状态,后29位代表是当前线程池中活动线程的数量,每当新增或者减少,只需要改动后29位中的值就可以了。
当有了这层理解之后,再看下面的execute方法就很容易了:
- 获取当前的ctl
- 根据ctl的后29位,判断当前线程数是否小于核心线程数,如果是,执行addWorker,直接返回
- 如果核心线程都被占满了,判断当前线程池是否是running状态,如果是,则通过offer方法,将当前任务添加至任务队列
- 再次尝试执行当前task,从缓存queue中取出该task,如果当前核心线程还是被占着,则调用addWorker,传入false参数,表示创建非核心线程
- 如果在第三步的时候,queue已经满了,无法添加,则直接调用addWorker启动非核心线程执行,如果执行失败,则执行拒绝策略。
所以,任务运行的核心就在于addWorker方法,我们一起来看下其源码,代码比较多,主要有两块内容,我们先看其中的第一部分:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
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
}
}
// 后面是第二部分,等下再讲
.
.
.
}
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
通过上面的解释,我们大概可以知道addWorker方法的功能,即在线程池中添加线程来执行,这一部分的内容写了两层自旋,在多线程环境下,通过一层层的状态判断,最终目的就是为了执行compareAndIncrementWorkerCount方法,也就是在后29位的数字基础上+1,表示当前活跃的线程数量+1。
也就是,只有在判断线程池中能够创建新线程,之后,才能去执行task,这个也很好理解。
下面看第二部分:
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;
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();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
主要的逻辑有下面几步:
- 将当前任务封装为一个Worker,怎么封装的呢,见Worker的构造器,很简单,在创建线程池的时候,有一个参数代表的是如何创建thread的ThreadFactory,创建出一个线程之后,将当前的任务绑定到该thread
- 执行:workers.add(w); 将新建的worker放入workers集合中去
- 添加成功之后,执行:t.start();执行当前worker绑定的线程
到此处,我们往线程池中添加任务,线程池执行任务,就完成了。
线程复用
等等,似乎少了点什么,是什么呢?
我们回顾下上面的过程,在上面的过程中,线程总是被ThreadFactory创建出来,然后绑定task,然后在执行task。
是不是跟我们锁理解的线程池有点不一样呢?我们使用线程池的目的就是为了实现线程的复用,而目前看下来,线程池所做的事情,是在不停的new线程,什么时候复用的呢??
是不是很困惑呢?
我们知道,Thread.start()只能调用一次,一旦这个调用结束,则该线程就到了stop状态,不能再次调用start。
所以要实现复用,只能是在run方法里面做文章了,线程池是怎么做的呢?
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
由此我们可以看到,线程的run实际最终执行的是runWorker方法,看到了吗,while 循环!是不是很兴奋?
没错,当线程创建出来之后,如果当前线程上有绑定task,则执行当前task,如果没有绑定task,则根据getTask方法去获取task,然后执行task。
再追踪下getTask方法:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
我们看到了什么?workQueue.poll !!任务队列啊,简直热泪盈眶。
至此,我们便弄清楚了线程池是如何创建线程以及线程复用的。
- 如果线程池为空,或者活跃的线程数量<核心线程数,或者缓存队列已满,但活跃线程数量没有超过最大线程数时,创建新线程,在创建线程的过程中,绑定当前的task,并执行
- 当前task执行完毕之后,该线程并没有退出,而是再次从任务缓存列队中取出第一条task继续执行,只要能获取到获取到任何一条task,当前Thread都不会退出,会一直执行下去