线程池
线程池
对线程统一分配、调优和监控
优点:
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
Java1.5中引入的 Executor 框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。
public class ExecutorCase{
// 初始化一个包含10个线程的线程池
private static Executor executor = Executors.newFixedThreadPool(10);
public static void main(String [] args){
for(int i=0;i<20;i++){
// 提交20个任务,每个任务打印当前线程名
executor.execute(new Task());
}
}
static class Task implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
}
核心参数
corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的 prestartAllCoreThreads() 方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;
keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize(产生阻塞)时才有用;
unit
keepAliveTime的单位;
workQueue
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在 JDK 中提供了如下阻塞队列:
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
- LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
- SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
- priorityBlockingQuene:具有优先级的无界阻塞队列;
threadFactory
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。
handler
排队策略和拒绝策略
排队策略:
1. 直接提交。直接提交策略表示线程池不对任务进行缓存。新进任务直接提交给线程池,当线程池中没有空闲线程时,创建一个新的线程处理此任务。这种策略需要线程池具有无限增长的可能性。实现为:SynchronousQueue
2. 有界队列。当线程池中线程达到corePoolSize时,新进任务被放在队列里排队等待处理。有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
3. 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
拒绝策略:
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
- AbortPolicy:直接抛出异常,丢弃任务,默认策略;
- CallerRunsPolicy:用调用者所在的线程来执行任务(可能造成当前线程阻塞);
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务(不抛异常);
- DiscardPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
Executors
Exectors工厂类提供了线程池的初始化接口,主要有如下几种:
-
newFixedThreadPool
初始化一个指定线程数的线程池,其中 corePoolSize == maximumPoolSize ,使用 LinkedBlockingQuene 作为阻塞队列,不过当线程池没有可执行任务时,不会释放线程。
-
newCachedThreadPool
- 初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用 SynchronousQueue 作为阻塞队列;
- 和 newFixedThreadPool 创建的线程池不同,newCachedThreadPool 在没有任务执行时,当线程的空闲时间超过 keepAliveTime ,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;
所以,使用该线程池时,一定要注意控制并发的任务数,否则创建大量的线程可能导致严重的性能问题。
-
newSingleThreadExecutor
初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行,内部使用LinkedBlockingQueue作为阻塞队列。
-
newScheduledThreadPool
初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。
实现原理
除了 newScheduledThreadPool 的内部实现特殊一点之外,其它几个线程池都是基于**ThreadPoolExecutor **类实现的。
线程池内部状态
/**
* java.lang.concurrent.ThreadPoolExecutor
*/
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; //32 - 3
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
//111 29*0 remain
private static final int RUNNING = -1 << COUNT_BITS;
//000 29*0 remain
private static final int SHUTDOWN = 0 << COUNT_BITS;
//001 29*0 remain
private static final int STOP = 1 << COUNT_BITS;
//010 29*0 remain
private static final int TIDYING = 2 << COUNT_BITS;
//011 29*0 remain
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) {
return c & ~CAPACITY; }
private static int workerCountOf(int c) {
return c & CAPACITY; }
private static int ctlOf(int rs, int wc) {
return rs | wc; }
其中AtomicInteger变量ctl的功能非常强大:利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态:
1、RUNNING:-1 << COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
2、SHUTDOWN: 0 << COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
3、STOP : 1 << COUNT_BITS,即高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
4、TIDYING : 2 << COUNT_BITS,即高3位为010;
5、TERMINATED: 3 << COUNT_BITS,即高3位为011;
任务提交
线程池框架提供了两种方式提交任务,根据不同的业务需求选择不同的方式。
-
Executor.execute()
通过Executor.execute()方法提交的任务,必须实现Runnable接口,该方式提交的任务不能获取返回值,因此无法判断任务是否执行成功。
-
ExecutorService.submit()
通过ExecutorService.submit()方法提交的任务,可以获取任务执行完的返回值。
任务执行
当向线程池中提交一个任务,线程池会如何处理该任务?
-
execute实现
/** * Executes the given task sometime in the future. The task * may execute in a new thread or in an existing pooled thread. * * If the task cannot be submitted for execution, either because this * executor has been shutdown or because its capacity has been reached, * the task is handled by the current {@code RejectedExecutionHandler}. * * @param command the task to execute * @throws RejectedExecutionException at discretion of * {@code RejectedExecutionHandler}, if the task * cannot be accepted for execution * @throws NullPointerException if {@code command} is null */ 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. * 1. 如果当前running线程数小于corePoolSize,则以 command 作为第一个任务启动一个新的线程 * 在执行addWorker()前,要检查线程池状态是否为RUNNING和当前线程池活动的线程数量, * 通过返回 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. * 2. 如果一个任务成功进入了阻塞队列,那么我们仍然需要二次检查是否应该添加一个线程 * (因为上次检查后会有线程died)或者进入本方法之后线程池关闭了。所以重新检查线程池状态: * 1.如果线程池停止了是否有回滚入队操作的必要, * 2.或者当没有活动线程的时候是否需要启动一个新的线程。 * * 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. * 3. 如果任务无法进入阻塞队列(阻塞队列已满),我们会试图创建一个新的线程来执行任务。 * (workerCountOf(c) > corePoolSize && workerCountOf(c) < maximumPoolSize) * 如果创建新线程失败了,我们就知道线程池已经关闭了,或者线程数已经饱和了 * 所以我们就拒绝这个任务(详见*拒绝策略) */ int c = ctl.get(); if (workerCountOf(c) < corePoolSize) { //活动线程数小于核心线程数 //创建新的线程执行当前任务,这个线程会用死循环监听 workQueue if (addWorker(command, true)) //true 代表未达到 corePoolSize return; c = ctl.get(); } //创建线程不成功,或者已经达到 corePoolSize ,把任务丢进阻塞队列 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); //试图创建空线程去监听 workQueue } //无法进入阻塞队列(已满),试图创建新线程(false代表当前线程数超过了 corePoolSize) else if (!addWorker(command, false)