> 文章列表 > 精通线程池,看这一篇就够了

精通线程池,看这一篇就够了

精通线程池,看这一篇就够了

一:什么是线程

当我们运用多线程技术处理任务时,需要不断通过new的方式创建线程,这样频繁创建和销毁线程,会造成cpu消耗过多。那么有没有什么办法避免频繁创建线程呢?
当然有,和我们以前学习过多连接池技术类似,线程池通过提前创建好线程保存在线程池中,在任务要执行时取出,任务结束时再放回去,由此大大提高线程利用率,避免频繁创建销毁带来的开销

二:Java提供的线程池有哪些

那么我们怎么才能创建一个线程池呢?可以通过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操作进行赋值

五:execute方法

public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();        //拿到32位intif (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位intif (! isRunning(recheck) && remove(command))//  4 isRunning(recheck)再次判断是否是Running//  5 如果不是Running,remove(command)移除任务reject(command);else if (workerCountOf(recheck) == 0)       //  6 获取当前工作的线程个数,如果是0addWorker(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))