在面试过程中,并发编程是绕不过去的话题,其中线程池是其中的重中之重,所以今天就来说下关于线程池的那些事。本人以前曾经使用多线程实现过表格导入,但是经过一段时间的学习之后,发现当时的实现方式简直太蠢了,也希望以此记录下
创建多线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 使用线程池
线程池的创建方式
(1)Executors.newFixedThreadPool(5):创建固定数量的线程池(一池固定线程数)
原理:创建一个线程池,核心线程数为5,最大线程数也为5,当任务来了,先交给常驻核心线程来执行,超过核心线程数的任务将会放到阻塞队列中(这里只简单说下,后面会详细解释下线程池工作过程)
new ThreadPoolExecutor(
nThreads,//核心线程数
nThreads,//最大线程数(与核心线程数相同)
0L, //存活时间(非核心线程的存活时间)
TimeUnit.MILLISECONDS,//存活时间的单位
new LinkedBlockingQueue<Runnable>()//阻塞队列(存放任务)
);
(2)Executors.newSingleThreadExecutor():创建单个线程的线程池
原理:创建一个线程池,核心线程数为1,最大线程数也为1,当任务数超过最大线程数池时,将任务放置到队列中,保证只有一个线程在工作,从而保证按照指定的顺序来执行
new ThreadPoolExecutor(
1,//核心线程数
1,//最大线程数(与核心线程数相同)
0L, //存活时间(非核心线程的存活时间)
TimeUnit.MILLISECONDS,//存活时间的单位
new LinkedBlockingQueue<Runnable>()//阻塞队列(存放任务)
);
3)Executors.newCachedThreadPool():创建可以缓存的线程
原理:创建一个线程池,核心线程数为0,最大线程数Integer.MAX_VALUE即初始化线程池时并没有可用线程,当来了任务之后再进行创建线程去执行;当执行第二个任务时,如果第一个任务已完成(未被回收时),则直接复用第一个任务的线程;如果第一个任务未完成,则创建第二个线程来执行第二个任务;默认可以创建无限多个任务线程
new ThreadPoolExecutor(
0,//核心线程数
Integer.MAX_VALUE,//最大线程数(与核心线程数相同)
60L, //存活时间(非核心线程的存活时间)
TimeUnit.MILLISECONDS,//存活时间的单位
new LinkedBlockingQueue<Runnable>()//阻塞队列(存放任务)
);
(4)newScheduledThreadPool:是可以设置定时执行的线程池,这个不常使用,不做过多解释
(5)手动创建线程池
/**
* ThreadPoolExecutor参数介绍
* int corePoolSize,----线程池中的常驻核心线程数,即可以立即执行线程的个数,类似 银行当值窗口
* int maximumPoolSize,----线程池能够容纳同时执行的最大线程数,即线程池最多可执行线程数,此致必须大于等于1,类似 银行所有窗口
* long keepAliveTime,----多余的空闲线程的存活时间
* 当前线程池数量超过corePoolSize时,当空闲线程达到keepAliveTime时,
* 多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
* <p>
* TimeUnit unit,----keepAliveTime的单位
* BlockingQueue<Runnable> workQueue,任务队列,被提交但尚未被执行的任务 类似 银行的排队座位数量
* ThreadFactory threadFactory,----表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
* RejectedExeutionHandler handle----拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时,将会拒绝执行部分线程
*/
ExecutorService executorService = new ThreadPoolExecutor(2,
5,
2,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
// new ThreadPoolExecutor.AbortPolicy()//当线程池不能再执行新的任务的时候丢弃任务并抛出RejectedExecutionException异常。
new ThreadPoolExecutor.CallerRunsPolicy()//当线程池不能再执行新的任务的时候,将会把新的任务返回给调用者,让调用者去执行
// new ThreadPoolExecutor.DiscardOldestPolicy()//当线程池不能执行新的任务的时候,将会抛弃等待时间最长的任务,然后重新尝试提交最新的任务
// new ThreadPoolExecutor.DiscardPolicy()//当线程之不能执行新的任务的时候,直接抛弃新任务,不执行也不报错
);
为啥不建议使用Executors去创建线程池的原因
(1) newFixedThreadPool和newSingleThreadExecutor主要问题是其阻塞队列的大小默认值为Integer.MAX_VALUE,即默认允许创建的线程数为Integer.MAX_VALUE,看似有界队列,实为无界队列,可能会创建大量的线程,从而导致OOM
(2) newCachedThreadPool和newScheduledThreadPool主要问题是其最大线程数默认为Integer.MAX_VALUE,可能会堆积大量的请求,这些请求会耗费大量的内存资源,从而导致OOM
线程池的七大参数
/* *ThreadPoolExecutor参数介绍
* int corePoolSize:线程池中的常驻核心线程数,即可以立即执行线程的个数,类似 银行当值窗口
* int maximumPoolSize:线程池能够容纳同时执行的最大线程数,即线程池最多可执行线程数,此致必须大于等于1,类似 银行所有窗口
* long keepAliveTime:,----多余的空闲线程的存活时间
* 当前线程池数量超过corePoolSize时,当空闲线程达到keepAliveTime时,
* 多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
* <p>
* TimeUnit unit:-keepAliveTime的单位
* BlockingQueue<Runnable> workQueue:任务队列,被提交但尚未被执行的任务 类似 银行的排队座位数量
* ThreadFactory threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
* RejectedExeutionHandler handle:-拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数时,将会拒绝执行部分线程
*/
线程池的参数理解
上面的线程参数要理解的话,我想有个例子是很合适的,那就是银行窗口
常驻核心线程数----当值窗口
非核心线程数-------暂停营业窗口
最大线程数---------所有窗口=当值窗口+暂停营业窗口
空闲线程存活时间-------暂停营业窗口临时营业时间
单位--------暂停营业窗口临时营业时间单位
阻塞队列------------等候区
拒绝策略-------------大堂经理,来给新来的群众解释拒绝办理业务的原因和方案
正常情况下,银行当值窗口开了4个,加上等候区有7个位置,可满足对11个群众服务
当值窗口未满时,新来群众直接去窗口办理业务
当值窗口坐满时,新来群众要到等候区等待
某天,业务异常火爆,当值窗口已满,等候区已满,此时还有群众来办业务
大堂经理就要去打电话给休息的同事,同时开启暂停营业的窗口(最大线程数开满),并说如果在30分钟(存活时间)内窗口没业务,你可以继续休息,窗口关闭
然而7个窗口+7个等候区都不能满足时,大堂经理就要将群众拒之门外了(拒绝策略)
线程池的拒绝策略
new ThreadPoolExecutor.AbortPolicy()//当线程池不能再执行新的任务的时候丢弃任务并抛出RejectedExecutionException异常。
new ThreadPoolExecutor.CallerRunsPolicy()//当线程池不能再执行新的任务的时候,将会把新的任务返回给调用者,让调用者去执行
new ThreadPoolExecutor.DiscardOldestPolicy()//当线程池不能执行新的任务的时候,将会抛弃等待时间最长的任务,然后重新尝试提交最新的任务
new ThreadPoolExecutor.DiscardPolicy()//当线程之不能执行新的任务的时候,直接抛弃新任务,不执行也不报错
实现RejectedExecutionHandler接口,可自定义处理器
线程池的底层工作原理
(1)在创建了线程池后,等待提交过来的任务请求
(2)当调用execute()方法添加一个请求任务时,线程池会做如下判断
-2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
-2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
-2.3 如果此时队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
-2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
(3)当一个线程完成任务时,它会从队列中取下一个任务来执行
(4)当一个线程无事可做超过一定的随时间(keepAliveTime)时,线程池会判断
如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉
如何确定最大线程数
分析一般从几个角度考虑:
1.任务的性质:CPU密集型的任务、IO密集型任务、混合型任务。
2.任务的优先级:高、中、低
3.任务执行时间:长、中、短
4.任务的依赖性:是否依赖其它系统资源,如数据库的连接等。
根据不同的任务可以交给不同规模的线程池执行。
如果是cpu密集型的,尽量减少线程数,如果是IO密集型任务尽量加大线程数,因为io不占用cpu的资源。建议配置2倍CPU个数+1。
如果是混合型的,尽量根据实际情况进行拆分,根据运行时间来决定。
如下为一般计算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
(2)并发不高、任务执行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以适当加大线程池中的线程数目,让CPU处理更多的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
这里也推下我的公众号:总要有一个梦想或大或小
最新文章会第一时间发到公众号,感谢关注
欢迎评论指正