线程池专题

背景

  线程池的基本介绍、为什么使用线程池以及使用线程池的配置等基础篇可以参考我之前的一篇博文:JAVA 线程池基本总结

  今天这里主要针对面试相关的再进行一次有针对性的整理和总结,每个细节点都是被问到过的,所以每个细节点都需要搞明白,搞透。

面试题目

  来看一下灵魂 5 连问

  1、什么是线程池?

  2、说一下 java 提供哪几种线程池?以及对应底层所使用的是什么队列?

  3、线程池据你了解有哪些好处?

  4、说一下工作中是如何使用线程池?以及起执行原理(意思就是:如何执行、如何拒绝以及如何关闭)

  5、缓存池大小你是怎么设定的?依据以及场景是什么

1、什么是线程池

  线程池的基本思想是一种对象池,在程序启动时就开辟一块内存空间,里面存放了众多(未死亡的)线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

2、java 提供的四种线程池

  平时工作中是通过 Executors 提供的四种线程池来应用的。接下来分别介绍这 4 中线程池

 1、FixedThreadPool():定长线程池

  • 可控制线程最大并发数(同时执行的线程数)
  • 超出的线程会在队列中等待

  调用以及源码如下:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 源码
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

  可以看到,corePoolSize 和 maximumPoolSize 的大小是一样的,keepAliveTime 和 unit的设值表明什么?- 就是该实现不想 keep alive!最后的 BlockingQueue 选择了 LinkedBlockingQueue,该 queue 有一个特点,他是一个无界队列

 2、SingleThreadExecutor():单线程化的线程池

  • 有且仅有一个工作线程执行任务
  • 所有任务按照指定顺序执行,即遵循队列的入队出队规则

  调用以及源码如下:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 源码
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

  这里和 fixedThreadPool 类似,corePoolSize 和 maximumPoolSize 的大小是 1,但是创建的线程池又被一个 FinalizableDelegatedExecutorService 包装了一下,如果不看FinalizableDelegatedExecutorService 中的源代码会让人不明白在这是干啥用的,具体源码我这里不粘贴了,有兴趣的同学可以自己去查看源码,我这里只讲一下关键点:

  它很简单, 只是继承了DelegatedExecutorService类并增加了一个finalize方法,即:

  protected void finalize() {
      super.shutdown();
  }

   finalize方法会在虚拟机利用垃圾回收清理对象时被调用,换言之,FinalizableDelegatedExecutorService 的实例即使不手动调用shutdown方法关闭现称池,虚拟机也会帮你完成此任务,不过从严谨的角度出发,我们还是应该手动调用shutdown方法,毕竟Java的finalize不是C++的析构函数,必定会被调用,Java虚拟机不保证finalize一定能被正确调用,因此我们不应该依赖于它。

 3、CachedThreadPool():可缓存的线程池

  • 线程数无限制
  • 有空闲线程则复用空闲线程,若无空闲线程则新建线程 一定程序减少频繁创建/销毁线程,减少系统开销

  调用以及源码如下:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 对应源码
public static ExecutorService newCachedThreadPool() {
  return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
              60L, TimeUnit.SECONDS,
              new SynchronousQueue<Runnable>());
}

  这里涉及到无界线程池,可以进行自动线程回收。

  这个实现就有意思了。

  1. 首先是无界的线程池,所以我们可以发现 maximumPoolSize 为 int 最大值即:Integer.MAX_VALUE

  2. 其次 BlockingQueue 的选择上使用 SynchronousQueue。可能对于该 BlockingQueue 有些陌生,简单说也叫直接提交:该QUEUE中,每个插入操作必须等待另一个线程的对应移除操作。

 4、ScheduleThreadPool()

  • 定时线程池。
  • 支持定时及周期性任务执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 源码
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

由于篇幅的原因,针对线程池的工作队列类型以及如有界、无界队列的详解参看我的另外一篇博文:线程池底层队列详解

3、使用线程池的好处

  • 提高响应速度:减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  • 防止资源过度使用:运用线程池能有效的控制线程最大并发数,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
  • 提高线程的可管理性:对线程进行一些简单的管理,比如:延时执行、定时循环执行的策略等,运用线程池都能进行很好的实现

4、工作中线程池的使用

  工作中使用 ThreadPoolExecutor 来完成线上线程池的构建与应用,当然 ThreadPoolExecutor 是 Executors 类的底层实现。具体先来看一下 ThreadPoolExecutor 完整的构造方法:

ThreadPoolExecutor(
    int corePoolSize,//池中所保存的线程数,包括空闲线程
    int maximumPoolSize, //池中允许的最大线程数
    long keepAliveTime,//当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间
    TimeUnit unit, //参数的时间单位
    BlockingQueue<Runnable> workQueue, //执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务
    ThreadFactory threadFactory, //执行程序创建新线程时使用的工厂
    RejectedExecutionHandler handler //由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
) 

贴出公司真实环境中使用的代码如下:

private ExecutorService executor = new ThreadPoolExecutor(
    50,
    150,
    1L,
    MINUTES,
    new LinkedBlockingQueue<>(100),
    threadFactory,
    (r, executor) -> {
        throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + executor.toString());
    }
);

我们可以通过 execute() 或 submit() 两个方法向线程池提交任务,不过它们有所不同

  • execute()方法没有返回值,所以无法判断任务知否被线程池执行成功
  • submit() 方法返回一个 future,那么我们可以通过这个 future 来判断任务是否执行成功,通过 future 的 get 方法来获取返回值

线程池的关闭策略

  我们可以通过 shutdown() 或 shutdownNow() 方法来关闭线程池,不过它们也有所不同

  • shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
  • shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止(如果线程中有sleep、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的)。shutdownNow会首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

线程池执行策略

  • 线程数量未达到 corePoolSize,则新建一个线程(核心线程) 执行任务
  • 线程数量达到了 corePoolSize,则将任务移入队列等待
  • 队列已满,新建线程(非核心线程) 执行任务
  • 队列已满,总线程数又达到了maximumPoolSize,就会由(RejectedExecutionHandler) 抛出异常或执行你设置的 rejectedHandler(拒绝策略)

线程池的四种拒绝策略

  1. AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满,线程池默认策略
  2. DiscardPolicy:不执行新任务,也不抛出异常,基本上为静默模式
  3. DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
  4. CallerRunPolicy:用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。

5、不同场景设置不同线程池大小

 1、高并发、任务执行时间短的业务

  线程池线程数可以设置为:CPU核数+1,减少线程上下文的切换

 2、并发不高、任务执行时间长的业务

  这个需要判断执行时间是耗在哪个地方

  • 假如是业务时间长集中在 IO 操作上,也就是 IO 密集型的任务,因为 IO 操作并不占用 CPU,所以不要让所有的 CPU 闲下来,可以适当加大线程池中的线程数目(2 * CPU核数),让 CPU 处理更多的业务。

  • 假如是业务长时间集中在计算操作上,也就是CPU密集型任务,最好设置为:CPU核数+1 ,线程池中的线程数设置得少一些,减少线程上下文的切换

 3、并发高、业务执行时间长的业务

  解决这种类型任务的关键不在于线程池而在于整体架构的设计。例如如何将多个执行时间长的业务进行拆分,拆分为更小的子块,使得子块执行时间降下来,实际应用中可以使用例如水平切分或者垂直切分等方案来降低子块的执行时间等。

 

posted @ 2020-04-27 19:14  星火燎原智勇  阅读(422)  评论(0编辑  收藏  举报