龙空技术网

细数线程池五大坑,一不小心线上就崩了

充满元气的java爱好者 835

前言:

而今同学们对“多线程拒绝策略什么意思”都比较重视,各位老铁们都需要学习一些“多线程拒绝策略什么意思”的相关资讯。那么小编在网络上网罗了一些关于“多线程拒绝策略什么意思””的相关资讯,希望各位老铁们能喜欢,看官们快快来学习一下吧!

系统性能优化的几种常用手段是异步和缓存。因此我们常常使用线程池异步处理一些业务。

线程池的使用还是相对比较简单的,首先创建一个线程池,然后通过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------

原文链接:

标签: #多线程拒绝策略什么意思 #线程池过大会怎样 #线程池使用注意坑 #线程池有什么缺点