线程池是什么
线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
当然,使用线程池可以带来一系列好处:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
线程池解决的问题是什么
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:
- 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
- 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。
“池化”思想不仅仅能应用在计算机领域,在金融、设备、人员管理、工作管理等领域也有相关的应用。
在计算机领域中的表现为:统一管理IT资源,包括服务器、存储、和网络资源等等。通过共享资源,使用户在低投入中获益。除去线程池,还有其他比较典型的几种使用策略包括:
- 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
- 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
- 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。
Java中的线程池
Java中的线程池是由ThreadPoolExecutor 来实现,继承关系如下图
ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:
(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;
(2)提供了管控线程池的方法,比如停止线程池的运行。
AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。比如AbstractExecutorService中的submit方法
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
ThreadPoolExecutor介绍
线程池的构造
ThreadPoolExecutor的构造方法有七个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
- corePollSize:核心线程池,当核心线程池没有满的时候,每来一个任务都会创建一个线程
- maximumPoolSize:最大线程池数,当线程池活跃线程数等于最大线程数,下一个任务来,不会创建线程去执行任务,而是执行拒绝策略
- keepAliveTime:线程的存活时间,当线程执行完任务后会存活keepAliveTime的时间,看看还有没有任务要执行。如果没有线程就会被回收
- unit:keepAliveTime的时间单位,秒,毫秒等
- workQueue:阻塞队列,管理任务的地方,当核心线程池满了,后面的任务会进入到workQueue进行等待
常用的阻塞队列有
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
可以看到ArrayBlockingQueue是有界阻塞队列,而LinkedBlockingQueue不传参数的话,默认大小是Integer.MAX_VALUE可以视为是无限的阻塞队列
- threadFactory:线程工厂,创建线程的地方,不传的话,默认是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();
}
我们可以通过实现ThreadFactory接口,来自定义线程工厂,这样就可以按照自己的需要定义线程池的名字,再把自定义的线程工厂传入构造参数中,这样线程池创建线程时,就会从我们的工厂创建线程
static class CustomThreadFactory 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 = "customPool-" +
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;
}
}
- handler:拒绝策略,当超过最大线程数时,任务提交就会被拒绝,从而执行拒绝策略,线程池中有四种拒绝策略
- CallerRunsPolicy:提交任务的线程来执行该任务
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
- AbortPolicy(默认):直接返回异常,该策略是默认的拒绝策略
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
- DiscardPolicy:什么都不做
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
- DiscardOldestPolicy:丢弃最老的任务,然后线程executor任务
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
可以看见上面四种策略都是实现了RejectedExecutionHandler这个接口,所以我们也可以通过实现该接口,来自定义符合我们要去的拒绝策略,比如当线程满的时候,任务一直等待队列有位置把任务放进队列
public static class CustomRejectHandle implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()){
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
线程池的生命周期
线程池创建出来了,接下来说一说线程池的生命周期
在线程池内部,通过一个变量 ctl来维护运行状态(runState)和线程数量(workerCount),在线程池内部,runState是通过高三位表示,而workerCount是低29位表示
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;//29
//00011111 11111111 11111111 11111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
//11100000 00000000 00000000 00000000
private static final int RUNNING = -1 << COUNT_BITS;
//00000000 00000000 00000000 00000000
private static final int SHUTDOWN = 0 << COUNT_BITS;
//00100000 00000000 00000000 00000000
private static final int STOP = 1 << COUNT_BITS;
//01000000 00000000 00000000 00000000
private static final int TIDYING = 2 << COUNT_BITS;
//01100000 00000000 00000000 00000000
private static final int TERMINATED = 3 << COUNT_BITS;
// ~CAPACITY:11100000000000000000000000000000
//计算当前的运行状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
//计算当前的线程数量
private static int workerCountOf(int c) { return c & CAPACITY; }
通过状态和线程数生成ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }
可以看到线程池有五种状态,2的三次方能表示八种状态,所以用高三位来表示运行状态,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。
线程池的生命周期的转换:
线程池的运行机制
线程池创建完成后,通过execute方法就能够执行一个任务,我们来看一看execute内部都做了什么事
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);
}
1.从源码可以看出execute先是对任务进行判空,不能传空任务进来,接着会去判断当前的线程数量是否小于corePoolSize,如果小于则创建线程去执行该任务并直接返回,如果创建失败则拿到当前的ctl,个人认为会失败的原因是execute是没有加锁的,可能workerCountOf(c) < corePoolSize满足,但是当执行到**addWorker(command, true)**时,其他线程已经把核心线程池加满了,导致再创建核心线程池的线程失败。
2. 如果当前线程数大于等于corePoolSize或者创建失败,则继续往下判断,如果当前的线程池状态是RUNNING,就把任务添加到阻塞队列中,offer是不阻塞的即队列满了就返回false,如果添加成功了,还需要再次检查线程池的状态,如果线程池被关闭了,就把刚刚添加到队列的任务移除,并执行拒绝策略。如果是RUNNING状态但是当前没有线程了,就会创建一个非核心线程池的线程,为的就是刚刚添加到队列的任务能够被执行,同时避免corePoolSize为0的情况。
3.如果添加队列失败,就会创建非核心线程池的线程去执行任务,如果创建失败,执行拒绝策略。
简单的用流程图表示