当我们运用多线程技术处理任务时,需要不断通过new的方式创建线程,这样频繁创建和销毁线程,会造成cpu消耗过多。那么有没有什么办法避免频繁创建线程
呢?
当然有,和我们以前学习过多连接池技术类似,线程池通过提前创建好线程保存在线程池中,在任务要执行时取出,任务结束时再放回去
,由此大大提高线程利用率,避免频繁创建销毁带来的开销
那么我们怎么才能创建一个线程池呢?可以通过Executors的以下方法创建
newFixedThreadPool 固定线程池数量
newSingleThreadExecutor 只有一个线程的线程池
newCachedThreadPool 可以缓存的线程池
newScheduledThreadPool 按周期执行的线程池
例如
ExecutorService executorService = Executors.newFixedThreadPool(3);//创建一个拥有三个线程的线程池
这些方法可以创建线程池,但是实际工作中并不推荐
使用这种方式,因为这里阻塞队列使用的是LinkedBlockingQueue
,是无界的,如果不断有任务添加进去,占用内存越来越多,可能导致OOM
所以更多时候,可以通过手动创建线程池
那么如何手动创建线程池呢?可以先点开上面提到的几个方法,会发现这些方法本质上都是最后构造一个ThreadPoolExecutor
实例
如下是Executors的newFixedThreadPool方法
public class Executors {
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
所以手动创建线程池,只需要创建ThreadPoolExecutor
就可以了,在创建之前,我们先要弄懂构造方法中的参数含义
,才能创建合适的线程池
从以上源代码中可以看到构造ThreadPoolExecutor,需要一些参数,那么这些参数分别是什么意思呢?先看一下ThreadPoolExecutor的构造方法
public ThreadPoolExecutor(int corePoolSize, //控制核心线程数
int maximumPoolSize,//控制最大线程数(核心线程+救急线程)
long keepAliveTime,//生存时间:针对救急线程#这里是一个数字
TimeUnit unit,//时间单位#这里可以是秒,毫秒等
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//可以为线程创建时起个好名字
RejectedExecutionHandler handler)//拒绝策略
那么什么是核心线程?什么又是救急线程呢?
核心线程: 执行完任务后需要保留在线程池中的
救急线程: 线程执行任务后不需要保留在线程池中的线程
阻塞队列: 对任务做缓冲作用,例如三个核心线程都在执行任务,这时候来了第四个任务怎么办?就将新任务放入workQueue队列中,等核心线程执 行完任务空闲了,就会从队列中获取任务
救急线程:如果核心线程已满,队列已满,这时候又来任务怎么办?就由救急线程来执行
拒绝策略:核心线程放满了,任务队列也满了,救急线程不能无限创建啊 这时候再来线程怎么办
线程池状态
ThreadPoolExecutor
使用int的高三位表示线程池状态
低29位表示线程数量
RUNNING 111
SHUTDOWN 000 线程池调用SHUTDOWN 方法,不会接受新任务,但是会处理阻塞队列中的任务
STOP 001 不会接受新任务,正在执行的任务也会停止,阻塞队列任务抛弃
TIDYING 010 任务执行完毕
TERMINATED 011 终结状态
这些信息存储在一个原子变量ctl中,目的是将线程池状态与
线程池个数合二为一,这样就可以通过一次CAS操作进行赋值
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get(); //拿到32位int
if (workerCountOf(c) < corePoolSize) { //workerCountOf(c)获取工作线程数 corePoolSize 核心线程数
if (addWorker(command, true)) //addWorker(command, true)创建核心线程数
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { // 1 isRunning判断线程池是否是Running状态
// 2 workQueue.offer(command) 将线程添加到阻塞队列
int recheck = ctl.get(); // 3 成功,再次Ctl.get ()拿到32位int
if (! isRunning(recheck) && remove(command))// 4 isRunning(recheck)再次判断是否是Running
// 5 如果不是Running,remove(command)移除任务
reject(command);
else if (workerCountOf(recheck) == 0) // 6 获取当前工作的线程个数,如果是0
addWorker(null, false); // 7 阻塞队列有任务,但是没有工作线程,添加一个任务为空
}
else if (!addWorker(command, false)) // 8 如果7的判断是running,创建非核心线程处理任务
reject(command); // 9 如果上一步创建失败 拒绝策略 reject(command);
}
其中拒绝策略在第三节讲参数的时候提到,那么具体有哪些拒绝策略
呢?
下图是拒绝策略的实现
AbortPolicy
(线程池默认的拒绝策略):丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。
必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
CallerRunsPolicy
:当触发拒绝策略,并且线程池没有关闭时,则使用父线程直接运行任务这会阻塞父进程继续往线程池中添加新的任务。个人认为仅仅适用于比较特殊的场景
DiscardPolicy
:直接丢弃,不抛出任何一场,适用于比较特殊的场景
DiscardOldestPolicy
:当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
通过以上,我们了解了线程池各个参数的含义,但是当我们自己创建线程池时,应该如何选择合适的参数呢?
这里需要重点考虑的就是:核心线程数
如何设置这里主要难点在于任务类型无法控制,例如:任务有CPU密集型
、IO密集型
CPU密集型
:系统硬盘、内存性能相对CPU要好很多,此时,系统运作 大部分状况是CPU Loading 100%,CPU读写IO(内存/硬盘)在短时间内可以完成,而CPU还有许多运算要处理CPU Loading很高
IO密集型
:CPU相对系统硬盘、内存性能要好很多,此时系统运作,大部分状况是CPU在等IO内存/硬盘)读写,此时CPU Loading 不高
IO密集型通常设置 2n+1,n是CPU核心数
CPU密集型通常设置为 n+1
实际中IO密集型较多,但是按照2n+的公式,在实际中可能不理想,如果增大线程数,会显著提高消息的处理能力
怎么判断需要增加更多线程呢?
可以使用jstack
命令查看进程的线程栈,如果线程池中线程都处于等待状态,说明线程够用, 如果大部分线程处于运行状态,可以适当调高线程数
可以套用这个公式:
线程数=CPU核心数/(1-阻塞系数(通常0.8))