一. 线程池的作用?
- 降低创建线程和销毁线程的性能开销
- 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
- 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题
二.有界和无界队列?
有界队列:就是有固定大小的队列。比如设定了固定大小的ArrayBlockingQueue,LinkedBlockingQueue,又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue。
无界队列:指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),就相当于 “无界”。比如没有设定固定大小的 LinkedBlockingQueue,一般都会设置容量大小,不然会造成内存溢出。
先说下任务缓存队列(workQueue),它用来存放等待执行的任务,他的类型是BlockingQueue,通常可以取下面三种类型:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
三.juc下的线程池体系,及几种常见的线程池?
1)newSingleThreadExecutor
创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓 在任务队列中。保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
2)newFixedThreadPool
该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交 时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
//核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中
nThreads, nThreads,
//超出核心 线程数量以外的线程空余存活时间
0L, TimeUnit.MILLISECONDS,
//LinkedBlockingQueue,使用的是默认容量Integer.MAX_VALUE,相当于没有上限
new LinkedBlockingQueue<Runnable>());
}
这个线程池执行任务的流程如下:
1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务
2. 线程数等于核心线程数后,将任务加入阻塞队列
3. 由于队列容量非常大,可以一直添加
4. 执行完任务的线程反复去队列中取任务执行
应用场景:FixedThreadPool用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量。
3)newCachedThreadPool
返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在60秒后自动回收。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,//核心线程数为0,缓存线程数无上限
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
线程池执行任务的流程如下:
1. 没有核心线程,直接向 SynchronousQueue 中提交任务
2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个
3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就被回收
应用场景:CachedThreadPool 用于并发执行大量短期的小任务,或者是负载较轻的服务器。
4)newScheduledThreadPool
创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize){
return new ScheduledThreadPoolExecutor(corePoolSize);
}
四.Executors使用工厂方法构建的四种线程池,都是基于ThreadpoolExecutor,它的核心构造参数?
五. ThreadpoolExecutor的任务执行(execute)流程?
源码分析
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {//1.当前池中线程比核心数少,新建一个线 程执行任务
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {//2.核心池已满,但任务队列未满,添加到队列中
int recheck = ctl.get();
//任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
if (! isRunning(recheck) && remove(command))
//如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
reject(command);
else if (workerCountOf(recheck) == 0)//如果之前的线程已被销毁完,新建一个线程
addWorker(null, false);
}else if (!addWorker(command, false))//3.核心池已满,队列已满,试着创建一个新线程
//如果创建新线程失败了,说明线程池被关闭或者线程池完全满了, 拒绝任务
reject(command);
}
execute和submit区别?
1. execute只可以接收一个Runnable的参数
2. execute如果出现异常会抛出
3. execute 没有返回值
1. submit 可以接收 Runable和Callable这两种类型的参数
2. 对于submit方法,如果传入一个Callable,可以得到一个Future的返回值
3. submit方法调用不会抛异常,除非调用Future.get
六.线程池的拒绝策略(RejectedExecutionHandler)的几种实现?
ThreadPoolExecutor中execute()处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
1) AbortPolicy:处理程序遭到拒绝将抛出运行时 RejectedExecutionException
// ThreadPoolExecutor中的默认拒绝策略
private static final RejectedExecutionHandler defaultHandler = new 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());
}
}
2) DiscardPolicy:也是丢弃任务,但是不抛出异常。
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
3)DiscardOldestPolicy:在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务(如果再次失败,则重复此过程)。这个策略需要适当小心。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
4)CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
七.如何合理配置线程池的大小?
线程池大小不是说越多越好,在遇到这类问题时,先进行分析
- 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
- 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉 及到网络传输以及底层系统资源依赖有关系
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu 的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行 线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上 下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1
如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态, 导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等 待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。 一个公式:线程池设定最佳线程数目 = (线程池设定的线程等待时间+线程 CPU 时间)/ 线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner 测试大量运行次数求出平均值)