目录
前言
本文内容线程池总结,包括线程池原理、线程池部分源码分析、线程池实践应用案例。
一、线程池原理
提示:首先我们可以从线程池的7大参数为入口,展开讲下 多种阻塞队列、多种拒绝策略的异同。其次讲利用Excutors创建线程池的几种创建方式及存在的内存溢出等问题,最后讲线程池的执行原理,新建线程及线程回收的原理。
1.线程池创建方式
Executors类为我们提供了各种类型的线程池,经常使用的方法有:
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
但是阿里开发手册是不允许使用Executors类创建的,强制使用ThreadPoolExecutor创建线程池。
Executors创建线程池弊端如下:
newSingleThreadExecutor 和 newFixedThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
newCachedThreadPool 和 newScheduledThreadPool允许的创建的线程数为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2. 线程池7大参数
线程池构造方法之一:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
① corePoolSize:核心线程的数量,默认不会被回收掉,但是如果设置了allowCoreTimeOut为true,那么当核心线程闲置时,也会被回收。
② maximumPoolSize :线程池能容纳的最大线程数量,上限被CAPACITY限制(2^29-1)。
③ keepAliveTime:闲置线程被回收的时间限制,也就是闲置线程的存活时间。
④ unit :keepAliveTime的单位
⑤ workQueue :用于存放提交但未执行的任务。
1.LinkedBlockingQueue:用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE.
//上限为Integer最大值,内存耗尽风险。
public LinkedBlockingQueue() { this(Integer.MAX_VALUE); }
2. ArrayBlockingQueue:用数组实现的有界阻塞队列,必须设置容量。
//队列容量可控,默认非公平锁
public ArrayBlockingQueue(int capacity) { his(capacity, false); }
//如果 fair==true 队列在插入或删除时访问阻塞的线程,按先进先出顺序处理,反之访问顺序未指定
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
⑥ threadFactory :创建线程的工厂类
⑦ handler:当任务执行失败时,使用handler通知调用者,代表拒绝的策略
JDK内置了四种拒绝策略:
-
DiscardOldestPolicy策略
:丢弃任务队列中最早添加的任务,并尝试提交当前任务; -
CallerRunsPolicy策略
:调用主线程执行被拒绝的任务,这提供了一种简单的反馈控制机制,将降低新任务的提交速度。 -
DiscardPolicy策略
:默默丢弃无法处理的任务,不予任何处理。 -
AbortPolicy策略
:直接抛出异常,阻止系统正常工作。(默认策略)
3.执行原理
见execute 源码分析
二、源码分析
1.走进线程池 execute 方法
由源码我们可以看出,execute 方法核心内容分为3步。上述注释里已经大致说明了 execute 方法的3步。
- 第1步:空处理,线程数小于核心线程数,成功创建核心线程加入任务并退出。
- 第2步:isRunning 方法判断线程池是否接受新任务并处理排队的任务 ,满足则将任务加入队列。
- 内层的 if-else 用来处理线程池不接受新任务和之前的线程已被销毁完的情况。
- 线程池不接受新任务的情况是拒绝任务;线程已被销毁完的情况是新建非核心线程。
- 第3步:核心线程数已满&&阻塞队列已满,则尝试创建非核心线程,失败则拒绝任务。
代码如下:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//如果当前线程数量小于核心线程数量,执行addWorker创建新线程执行command任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果当前是运行状态,将任务放入阻塞队列,double-check线程池状态
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//再次check,发现线程池状态不是运行状态了,移除刚才添加进来的任务并拒绝该任务
if (! isRunning(recheck) && remove(command))
reject(command);
//处于运行状态,但是没有线程,创建线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//尝试创建新的非核心线程,失败则reject任务
else if (!addWorker(command, false))
reject(command);
}
2.其他..
代码如下():
...
三、应用场景
1.一次请求里多次调用其他系统接口。
比如我们系统A生产环境,调用其他系统B的接口平均耗时100ms,但是一次请求中入参的list里有20条数据需要调用B系统。如果使用单线程方式,耗时=100ms*20=2秒,外加我们系统处理逻辑耗时+我们系统插入、修改以及查redis或者redis不存在时查库耗时,总的下来最少2秒多,但是生产接口是不允许耗时这么久的。因此就用线程池对该功能进行优化,将响应时间控制在300ms以内。
2.读取多个文件
四、总结
以上就是线程池的原理及源码分析,以及在何种情况下使用线程池的总结。
掌握:
- 线程池七大参数意义
- 线程池执行原理
- 何时新建、回收线程
- 应用场景
内容补充中...