0
点赞
收藏
分享

微信扫一扫

线程池管理

奔跑的酆 2021-09-29 阅读 67

频繁的开启销毁线程调用GC,会引起内存抖动,这在APP端是内存优化的一个方向。
使用线程池的优点:
1:重用线程池中的线程,线程在执行完任务后不会立刻销毁,而会等待另外的任务,这样就不会频繁地创建、销毁线程和调用GC。。
2:有效控制线程池的最大并发数,避免大量线程抢占资源出现的问题。
3:对多个线程进行统一地管理,可提供定时执行及指定间隔循环执行的功能。

思考几个面试题:

1.构造一个线程池为什么需要几个参数,分别是干什么用的?
2.线程池会出现oom吗,如何避免?
3.Runnable和Callable的区别是什么?

ThreadPoolExecutor

线程池概念来源于Java中的Executor,它是一个接口,真正的实现为ThreadPoolExecutor,ThreadPoolExecutor提供了一系列参数来配置线程池。
ThreadPoolExecutor 有多个重载方法,但最终都调用了这个构造方法。


*** @param corePoolSize the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set
线程池中核心线程的数量,除非设置了{@code allowCoreThreadTimeOut},即使它们处于空闲状态也要保留在池中的线程数。
*** @param maximumPoolSize the maximum number of threads to allow in the pool
线程池中最大线程数量
*** @param keepAliveTime when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
非核心线程的超时时长(不一定只表非核心线程),当系统中非核心线程闲置时间超过keepAliveTime之后,则线程会被回收任务关闭。 如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长
*** @param unit the time unit for the {
@code** keepAliveTime} argument
keepAliveTime这个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等。
*** @param workQueue the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method.
任务队列,在任务被执行之前用来持有任务的队列
*** @param threadFactory the factory to use when the executor creates a new thread
创建线程的工厂类,可用于设置线程名字等等,一般无须设置该参数。
*** @param handler the handler to use when execution is blocked because the thread bounds and queue capacities are reached
(线程饱和策略)当因为达到线程界限和队列容量,执行被阻止时用来处理的hander,设置默认是AbortPolicy,会抛出异常
两种情况会拒绝处理任务:
1.当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
2.当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
rejectedExecutionHandler根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
*** @throws IllegalArgumentException if one of the following holds:*
{@code corePoolSize < 0}
{@code keepAliveTime < 0}
{@code maximumPoolSize <= 0}
{@code maximumPoolSize < corePoolSize}
*** @throws NullPointerException if {@code workQueue}
or {@code threadFactory} or {@code handler} is null
IllegalArgumentExceptionIllegalArgumentException 是创建ThreadPoolExecutor时可能会抛出的异常。

线程池工作流程


从图中可以看出,线程分两种,一种是核心线程,一般是开启之后就常驻方便复用;另一种是非核心线程,使用完毕之后释放,不可复用。任务队列是针对核心线程,若池子的核心线程已满,队列已满,池子最大线程数已满(非核心线程也无法开启)就调用饱和策略。值得注意的是:如果可以,新增的任务首选肯定是核心线程(哪怕是先排队)然后才考虑非核心线程。

线程池选型

问题

1.corePoolSize maximumPoolSize设置不当会影响效率,甚至耗尽线程;
2.workQueue 设置不当容易导致OOM;
3.handler 设置不当会导致提交任务时抛出异常。
有一种观点是:
如果是CPU密集型应用,则线程池大小设置为N+1
如果是IO密集型应用,则线程池大小设置为2N+1
但是查看部分源码却不是这样




在实际选型中的线程数
有人分析过其他一些经典框架:
Picasso: 可以自己配置,没有就用默认在Picasso#builde()中



Picasso真正灵活的地方是可以根据网络状况调节线程池的大小,


从Glide和Picasso默认线程池来看,
corePoolSize == maximumPoolSize 且不宜过多,Glide取值Math.min(4, availableProcessors)可用cpu与4之间的小者,Picasso取值3然后根据网络环境调整wifi状态下为4。我们可以理解为核心线程是只要被创建就会一直被持有的不会销毁所以保持cup能够承受的且不影响性能的数量,而corePoolSize 与maximumPoolSize 相等就是线程池最好不允许有非核心线程(不被管理也不存在复用)存在,当然keepAliveTime(非核心线程闲置时长)也为0,Glide使用的是具有优先级的阻塞队列PriorityBlockingQueue,队列容量固定为11;


PS:Glide缓存池有多种,我只分析了SourceExecutor这个是用来处理显示的线程池,还有缓存和动画的,在此就不展开,有兴趣的可以去读一读其他类型的线程池。


饱和策略(handler):
RejectedExecutionHandler是一个接口,可以自己实现拒绝策略;也可以使RejectedExecutionHandler提供的常见策略有四种:
AbortPolicy(默认):抛出RejectedExecutionException;
DiscardPolicy:直接忽略什么都不做;
DiscardOldestPolicy:丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置;
CallerRunsPolicy:直接由提交任务者执行这个任务;

除了自定义线程池,jdk提供一些api来获取不同类型的线程池:
FixedThreadPool
特点:固定大小的线程池,参数为核心线程数,只有核心线程,无非核心线程无超时时长,并且阻塞队列无界。
适用:执行长期的任务


SingleThreadPool
特点:只有一个核心线程,当被占用时,其他的任务需要进入队列等待
适用:排队执行任务,一次只执行一个


CachedThreadPool
特点:不限线程数上限的线程池,任何提交的任务都将立即执行,不持有核心线程,只有非核心线程,并且每个非核心线程空闲等待的时间为60s,采用SynchronousQueue队列。
适用:执行很多短期异步的小程序或者负载较轻的服务器
思考:CachedThreadPool 无核心线程,SynchronousQueue的存在会不会冲突?
因为没有核心线程,其他全为非核心线程,SynchronousQueue是不存储元素的,每次插入操作必须伴随一个移除操作,一个移除操作也要伴随一个插入操作。
当一个任务执行时,先用SynchronousQueue的offer提交任务,如果线程池中有线程空闲,则调用SynchronousQueue的poll方法来移除任务并交给线程处理;如果没有线程空闲,则开启一个新的非核心线程来处理任务。
由于maximumPoolSize是Integer.MAX_VALUE,无界的,所以如果线程处理任务速度小于提交任务的速度,则会不断地创建新的线程,这时需要注意不要过度创建,应采取措施调整双方速度,不然线程创建太多会影响性能。


ScheduledThreadPool
特点:核心线程数量是固定的,非核心线程无穷大。
ScheduledThreadPool也是四个当中唯一一个具有定时定期执行任务功能的线程池。
适用:执行一些周期性任务或者延时任务。


线程池任务的终止:
一般线程执行完run方法之后,线程就正常结束了,线程池中的任务可以用下面的方式来实现:
利用 Future 和 Callable
1.实现 Callable 接口
2.调用 pool.submit() 方法,返回 Future 对象
4.用 Future 对象来获取线程的状态。


线程池的管理:
1.shutDown() 关闭线程池,不影响已经提交的任务
2.shutDownNow() 关闭线程池,并尝试去终止正在执行的线程
3.allowCoreThreadTimeOut(boolean value) 允许核心线程闲置超时时被回收
4.submit 一般情况下我们使用execute来提交任务,但是有时候可能也会用到submit,使用submit的好处是submit有返回值。
5.beforeExecute() - 任务执行前执行的方法
6.afterExecute() -任务执行结束后执行的方法
7.terminated() -线程池关闭后执行的方法
现在回顾前文提到的三个问题:
1.构造一个线程池为什么需要几个参数,分别是干什么用的?
七个参数,corePoolSize(核心线程数) maximumPoolSize(池子最大线程容量,核心+非核心) keepAliveTime(非核心或核心线程超时时长) unit(超时时长时间单位) workQueue(核心线程的等待队列) threadFactory(创建线程的工厂类) handler(饱和策略处理器)
2.线程池会出现oom吗,如何避免?
会,workQueue若无限大(无界队列)就会导致不停的缓存新建线程,就会导致oom,设置合理的队列容量即可避免,
而大部分的Extutors.newXXThreadPool()快捷创建队列都使用的无界队列
3.Runnable和Callable的区别是什么?
使用Runnable线程不可控,使用Callable线程可操作和获取返回值。

举报

相关推荐

0 条评论