ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-11-02 10:03:48  阅读:170  来源: 互联网

标签:细数 调用 get ThreadLocal 任务 线程 池五 执行


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

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

A方法内调用B方法:
A() {
   B();
}

现在父子任务都被同一个线程池进行调用,整个流程为(如图所示):
在这里插入图片描述

1、线程池创建核心线程,并执行A方法
2、执行到B方法时,将B交给线程池执行,由于没有多余线程,因此暂存队列
3、A任务等待B任务执行完,B任务等待A任务释放线程。从而互相等待,造成“饥饿”死锁

解决方案:

问题原因在于互相等待,因此只要保证类似的父子任务不要被同一线程池执行即可

------The End------

如果这个办法对您有用,或者您希望持续关注,也可以扫描下方二维码或者在微信公众号中搜索【码路无涯】

在这里插入图片描述

标签:细数,调用,get,ThreadLocal,任务,线程,池五,执行
来源: https://blog.csdn.net/zy1817204670/article/details/121072508

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有