什么是线程池
线程是一种昂贵的资源,其主要开销分为以下几个方面:
- 线程的创建和启动的开销
- 线程销毁的开销
- 线程调度的开销。线程的调度会导致上下文切换,从而增了了处理器资源的消耗
因此,我们需要一种有效的使用线程的方式,线程池就是一种常见的方式。
线程池的优势
- 降低创建线程和销毁线程的性能开销
- 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
- 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题
线程池的使用
线程池在java中是如何实现的呢?
在 JDK 1.5 之后推出了相关的 api,常见的创建线程池方式有以下几种:
- Executors.newFixedThreadPool(int nThreads):创建固定大小的线程池
- Executors.newCachedThreadPool():返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒
后自动回收 - Executors.newSingleThreadExecutor():创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中
- Executors.newScheduledThreadPool(int corePoolSize): 创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器
下面举一个例子,看看具体是如何使用的:
public class ExecutorDemo implements Runnable{
static ExecutorService es= Executors.newFixedThreadPool(1);
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
for(int i=0;i<100;i++) {
es.execute(new ExecutorDemo());
}
es.shutdown();
}
}
线程池的实现原理
上面提到的四种线程池的构建,都是基于 ThreadpoolExecutor 类来构建的,我们先来看看ThreadpoolExecutor 类的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:用于指定核心线程池的大小
- maximumPoolSize:用于指定最大线程池大小
- keepAliveTime:用于指定线程池中空闲线程的最大存活时间
- unit:存活时间单位
- workQueue:工作队列的阻塞队列
- threadFactory:用于指定创建工作者线程的线程工厂
- handler:当任务无法执行的时候的处理方式,拒绝策略
我们再来看看线程池的两个核心方法
注意:我会把源码中每个方法的作用都注释出来,可以参考注释进行理解。
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);
}
再方法逻辑之前,我们先来了解一下线程池有哪些状态:
private static final int COUNT_BITS = Integer.SIZE - 3;//32-3=29
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//state
private static final int RUNNING = -1 << COUNT_BITS;//111
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;
- RUNNING:自然是运行状态,指可以接受任务执行队列里的任务
- SHUTDOWN:调用了 shutdown() 方法,不再接受新任务了,但是队列里的任务得执行完毕
- STOP :调用了 shutdownNow() 方法,不再接受新任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务
- TIDYING:所有任务都执行完毕,线程数量为 0,处于该状态的线程池即将调用 terminated()方法
- TERMINATED:终止状态,terminated()方法执行完成
我们以RUNNING = -1 << COUNT_BITS为例,讲解一下位运算
COUNT_BITS=32
-1的二进制位1111 1111 1111 1111 1111 1111 1111 1111
-1 的二进制计算方法
原码是 1000 0000 0000 0000 0000 0000 0000 0001 . 高位 1 表示符号位,1表示负
对原码取反,高位不变得到 :1111 1111 1111 1111 1111 1111 1111 1110
对反码进行+1 ,也就是补码操作, 最后得到 1111 1111 1111 1111 1111 1111 1111 1111
然后向左有符号左移29位,也就是111
它们之间的状态转换如下图:
我们接着看execute方法,它主要分为三部分:
- 如果当前线程池中线程数少于核心线程数的时候,会执行addWorker方法去新建一个工作线程
- 如果核心线程数已经满了,但任务队列没满,将任务添加到队列中
- 如果核心线程数满了,任务队列也满了,创建一个临时线程
- 失败的话,执行reject拒绝策略
addWorker(Runnable firstTask, boolean core)
core的意思是:
- true:使用corePoolSize(核心线程池大小)为基准
- false:使用maximumPoolSize(最大线程池大小)为基准
private boolean addWorker(Runnable firstTask, boolean core) {
retry://goto语句,标记循环,避免死循环
//自旋
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//1 线程池已经 shutdown 后,还要添加新的任务,拒绝
//注意:SHUTDOWN 状态不接受新任务,但仍然会执行已经加入任务队列的任务
//2.当进入SHUTDOWN 状态,而传进来的任务为空,并且任务队列不为空的时候,是允许添加新线程
//3.,如果把这个条件取反,就表示不允许添加 worker
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {//自旋
获得工作线程数
int wc = workerCountOf(c);
//如果工作线程数大于默认容量大小
//或者大于核心线程数大小,则直接返回 false 表示不能再添加 worker。
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//CAS操作去增加线程数
//如果失败,自旋
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;
//加锁
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
// 只有线程是非SHUTDOWN 状态
//或者线程是SHUTDOWN 状态且firstTask ==null
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;
}
addWorker方法主要分为两部分:
- 采用自旋,通过 CAS 操作将线程数加 1
- 新建一个工作线程并启用
reject()拒绝策略
final void reject(Runnable command) {
handler.rejectedExecution(command, this);
}
总共有四种不同的实现方法:
- 直接抛出异常,默认使用此策略
实现类:ThreadPoolExecutor.AbortPolicypublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); }
- 丢弃当前被拒绝的任务(而不抛出任何异常)
实现类:ThreadPoolExecutor.DiscardPolicypublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }
- 将工作队列中最老的任务丢弃,然后重新尝试接纳被拒的任务
实现类:ThreadPoolExecutor.DiscardOldestPolicypublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } }
- 在客户端线程中执行被拒的任务
实现类:ThreadPoolExecutor.CallerRunsPolicypublic void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } }
其中ThreadPoolExecutor.AbortPolicy是ThreadPoolExecutor默认使用的Handle。如果默认的无法满足要求,会优先考虑ThreadPoolExecutor自身提供的另外三种策略,其次才会考虑自行实现的策略handle
最后,通过一张流程图来回顾一下线程池的整个过程
线程池的关闭
jdk提供了两个方法用于关闭线程池:
- shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务,将线程池的状态变为SHUTDOWN
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务,将线程池的状态变为STOP
线程池的监控
ThreadPoolExecutor类提供了对线程池进行监控的相关方法
- getPoolSize():获得当前线程池的大小
- getCorePoolSize():获得当前线程池核心线程数
- getLargestPoolSize():获得工作者线程曾经达到的最大数,有助于确认线程池大小设置是否合理
- getQueue():获得工作队列实例
- getActiveCount():获得线程池中正在执行任务的工作线程数量
- getTaskCount():获得线程池中到目前为止接收到的任务数(近似值)
- getCompletedTaskCount():获得线程池到目前为止处理完毕的任务数(近似值)
合理设置线程数
线程数不宜过小,过小可能导致无法充分利用资源;线程数也不宜过大,过大会增加上下文切换以及其他开销。如何设置一个合理的线程数呢?
首先,我们根据线程所执行的任务所消耗的资源来分类:
- CPU密集型任务:执行任务中消耗的主要是CPU时间,比如:加密和解密
- IO密集型任务:执行任务中消耗的主要是I/O资源(如网络和磁盘),比如:文件读写,网络读写等
- 混合型任务:同时包含CPU密集型任务和IO密集型任务,我们通常采用子任务的方式分别执行CPU密集型任务和IO密集型任务,提高并发性
根据分类,我们可以根据以下规则设置:
- 对于CPU密集型任务,线程数通常可以设置为Ncpu(CPU的数量)+1
- 对于IO密集型任务,优先考虑将线程数设置为1,在一个线程不够用的情况下将线程数向2*Ncpu靠拢
总结
在阿里巴巴开发手册上明确规定了线程池的用法