前言:
而今同学们对“多线程拒绝策略什么意思”都比较重视,各位老铁们都需要学习一些“多线程拒绝策略什么意思”的相关资讯。那么小编在网络上网罗了一些关于“多线程拒绝策略什么意思””的相关资讯,希望各位老铁们能喜欢,看官们快快来学习一下吧!系统性能优化的几种常用手段是异步和缓存。因此我们常常使用线程池异步处理一些业务。
线程池的使用还是相对比较简单的,首先创建一个线程池,然后通过execute或submit执行任务。
但魔鬼往往藏于细节之中,稍有不慎就会出错。本文将会详细总结线程池容易出错的五大坑
一、拒绝策略参数知多少
二、拒绝策略使用不当,系统阻塞不可用
三、多任务get()异常时,结果获取有误
四、ThreadLocal与线程池搭配使用,上下文缺失
五、父子任务共用同一线程池,系统“饥饿”死锁
以下为线程池的核心流程【具体内容参考:线程池原理】
一、拒绝策略参数知多少
我们都知道,当任务过多,线程池处理不过来时会被拒绝,进入拒绝策略
public interface RejectedExecutionHandler { void rejectedExecution(Runnable r, ThreadPoolExecutor executor);}
通过实现RejectedExecutionHandler,就可以作为线程池的拒绝策略使用。
目前官方提供了四种拒绝策略,分别为:
CallerRunsPolicy:由任务调用方执行AbortPolicy:抛出异常,同样也是由任务调用方处理异常DiscardPolicy:丢弃当前任务DiscardOldestPolicy:丢弃队列中最老的任务,并执行当前任务
线程池有execute和submit两种方法执行任务:
execute执行我们最原始的任务;
而submit则不同,先是将我们最原始的任务封装成FutureTask任务,然后将FutureTask任务交由execute执行
线程池拒绝策略中Runnable r就是execute执行的任务,因此当使用r时就要注意它是我们最原始的任务还是FutureTask任务
二、拒绝策略使用不当,系统阻塞不可用
前面我们讲到submit方法执行任务时,线程池会先封装任务到FutureTask中,然后我们通过FutureTask的get()方法获取任务处理的结果
【具体内容参考:一张动图,彻底懂了execute和submit】
Possible state transitions:
NEW -> COMPLETING -> NORMAL(任务执行完成)
NEW ->COMPLETING -> EXCEPTIONAL(任务抛出异常)
NEW -> CANCELLED(任务被取消)
NEW -> INTERRUPTING -> INTERRUPTED(任务被打断)
FutureTask在被创建时状态为NEW,任务执行到某个阶段就会修改成相应状态,直到达到最终态。
FutureTask根据状态变更来标识任务执行进度的,因此get()方法也是在状态达到最终态(任务执行成果/异常/被取消/被打断)时才能返回结果,否则挂起当前线程等待到达最终态。
问题原因:
1、当任务通过submit方法执行时,会创建FutureTask(此时状态为NEW)
2、任务被拒绝且拒绝策略为丢弃任务(DiscardOleddestPolicy或DiscardPolicy)时,任务直接被线程池丢弃(此时状态仍为NEW)
3、当执行get()方法时,由于任务一直处于NEW状态,没有达到最终态,线程会一直处于阻塞状态
解决方案:
问题原因在于:任务无法变成最终态,导致阻塞。
因此我们可以重写rejectedExecution方法,将任务置为最终态
FutureTask的cancel方法可以将任务状态置为CANCELLED或INTERRUPTED
public static RejectedExecutionHandler customDiscardPolicy () { return new DiscardPolicy() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { if (r != null && r instanceof FutureTask) { ((FutureTask) r).cancel(true); } } } };}
三、多任务get()异常时,结果获取有误
submit方法中,futureTask会捕获异常,在get()时抛出。
若批量执行多个方法,且for循环get()结果时,捕获异常要在循环内,而不是循环外。否则会影响其他任务的结果输出
捕获异常在循环外,当一个任务get异常时,后续其他任务就不能再获取结果
List<TaskResult> taskResultList = new ArrayList<>();try { for (Future<TaskResult> future : futureList) { if (future == null) {continue;} TaskResult result = future.get(); taskResultList.add(result); }} catch (Throwable t) { //这种场景下,当一个任务get异常时,后续其他任务就不能再获取结果 LOGGER.error("任务执行异常", t);}
因此在循环内捕获异常,各个任务互相不受影响
List<TaskResult> taskResultList = new ArrayList<>();for (Future<TaskResult> future : futureList) { try { if (future == null) {continue;} TaskResult result = future.get(); taskResultList.add(result); } catch (Throwable t) { LOGGER.error("任务执行异常", t); }}
四、ThreadLocal与线程池搭配使用,上下文缺失
ThreadLocal的使用一般都是这几个方法:
private final static ThreadLocal<CacheInfo> cacheInfoThreadLocal = new ThreadLocal<CacheInfo>();cacheInfoThreadLocal.set(cacheInfo);cacheInfoThreadLocal.get();cacheInfoThreadLocal.remove();
为防止内存泄漏,在使用完ThreadLocal后都会调用remove()清除数据
问题描述:
1、当任务需要调用方线程的ThreadLocal信息时,通用方式就是将调用方ThreadLocal信息赋值到执行任务的线程中,在任务执行结束后调用remove()清除数据
2、同时任务恰好被线程池拒绝,且使用的拒绝策略是CallerRunsPolicy时,任务会被调用方线程执行。
3、若此时任务执行结束后仍调用remove()清除数据,清除的就会是调用方的ThreadLocal数据。
调用方ThreadLocal数据被清除,数据丢失在工作中将会是灾难性的。
解决方案:
问题出现的原因是任务由于被拒绝,导致误删除了调用方ThreadLocal数据
因此可以在任务执行时判断执行线程是否为调用方线程。
若是则不用set()复制和remove()清空数据
public abstract class ParallelCallableTask<V> implements Callable<V> { //调用方线程名称 private String mainThreadName; public ParallelCallableTask() { mainThreadName = Thread.currentThread().getName(); } @Override public V call() throws Exception { //是否为同一线程 boolean sameThread = sameThread(); return proccess(sameThread); } /**判断 调用方线程 和 执行线程 是否为同一线程*/ private boolean sameThread () { String curThreadName = Thread.currentThread().getName(); return curThreadName.equals(mainThreadName); } //任务重写这个方法并根据sameThread判断是否需要set和remove调用方线程的ThreadLocal数据 public abstract V proccess(boolean sameThread);}
待执行的任务通过重写process方法,并根据sameThread判断是否和主线程一致,一致则不重复设置相同的threadLocal和删除threadLocal
五、父子任务共用同一线程池,系统“饥饿”死锁
A方法调用B方法,AB方法称为父子任务。
当他们都被同一个线程池执行时,一定条件下会出现以下场景:
1、父任务获取到线程池线程执行,而子任务则被暂存到队列中
2、当父任务占满了线程池所有的线程,等待子任务返回结果后,结束父任务
3、此时子任务由于在队列中,一直不能等到线程来处理,导致不能从队列中释放
4、父子任务互相等待,从而造成“饥饿”死锁
我们举一个简单例子:
假设线程池参数设置为:核心和最大线程数为1,队列容量为1A方法内调用B方法:A() { B();}
现在父子任务都被同一个线程池进行调用,整个流程为(如图所示):
1、线程池创建核心线程,并执行A方法
2、执行到B方法时,将B交给线程池执行,由于没有多余线程,因此暂存队列
3、A任务等待B任务执行完,B任务等待A任务释放线程。从而互相等待,造成“饥饿”死锁
解决方案:
问题原因在于互相等待,因此只要保证类似的父子任务不要被同一线程池执行即可
------The End------
原文链接:
标签: #多线程拒绝策略什么意思 #线程池过大会怎样 #线程池使用注意坑 #线程池有什么缺点