本文导读

  • 本文主要描述Java线程池的理论知识
  • Java中有几种方法新建一个线程?
  1. 继承Thread或者实现Runnable
  2. 使用更高级的线程池

线程池简述

  • 线程池是JDK1.5开始引入的,也叫Executor框架,或是Java并发框架
  • 线程池相关的API在java.util.concurrent包中,常用到以下几个类和接口:
  1. java.util.concurrent.Executor:一个只包含一个方法的接口,它的抽象含义是:用来执行一个Runnable任务的执行器
  2. java.util.concurrent.ExecutorService:继承了Executor接口的接口,增加了很多对于任务和执行器的生命周期进行管理的方法
  3. java.util.concurrent.ThreadFactory:一个生成新线程的接口。用户可以通过实现这个接口管理对线程池中生成线程的逻辑
  4. java.util.concurrent.Executors:创建并返回其余各个实例的类,提供了很多不同的生成执行器的实用方法,比如基于线程池的执行器的实现。
  5. java.util.concurrent.ThreadPoolExecutor:这个类维护了一个线程池,对于提交到此Executor中的任务,它不是创建新的线程而是使用池内的线程进行执行,对于数量巨大但执行时间很短的任务,可以显著地减少对于任务执行的开销。

Executor结构

  • executor结构主要包括任务、任务的执行和异步结果的计算。
  • 任务:包括被执行任务需要实现的接口,如Runnable接口或Callable接口
  • 任务的执行:包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)
  • 异步计算的结果:包括接口Future和实现Future接口的FutureTask类

使用线程池的好处

  • 降低资源消耗:可以重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控

线程池工作原理

  • 当一个新的任务提交到线程池之后,线程池处理过程如下:
  1. 线程池判断核心线程池里的线程是否已满。未满时,则创建一个新的工作线程来执行任务。如果核心线程池里的线程已满,则执行第二步。
  2. 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里等待执行。如果工作队列满了,则执行第三步。
  3. 线程池判断线程池(核心线程池外的线程池部分)的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

线程池饱和策略

  • 常用的饱和策略如下:
  • 它们是ThreadPoolExecutor类中的内部类,可以直接调用
线程池理论 1

AbortPolicy

  • Java线程池默认的阻塞策略,即不执行此新任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。

DiscardPolicy

  • 直接抛弃,新任务不执行,空方法

DiscardOldestPolicy

  • 从队列里面抛弃head的一个任务,并再次execute 此task。

用户自定义拒绝策略(最常用)

  • 实现RejectedExecutionHandler,并自己定义策略模式

线程池工作流程图

  • 以ThreadPoolExecutor为例展示线程池的工作流程

线程池理论 1

线程池理论 1

  • 如果当前运行的线程少于corePoolSize(核心线程数),则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue(阻塞队列/任务队列)。
  • 如果无法将任务加入BlockingQueue(队列已满),则在非corePool中创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
  • 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并执行线程饱和策略,如:RejectedExecutionHandler.rejectedExecution()方法。
  • ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

工作队列排队策略

  • 已经说过当线程池中工作线程的总数量超过核心线程数量后,新加的任务就会放入工作队列中进行等待被执行
  • 使用线程池就得创建ThreadPoolExecutor对象,通过ThreadPoolExecutor(线程池)类的构造方法创建时,就得指定工作队列,它是BlockingQueue<Runnable>接口,而实际开发中是指定此接口的具体实现类,常用的如下所示。

SynchronousQueue

  • 直接提交策略----意思是工作队列不保存任何任务被等待执行,而是直接提交给线程进行执行。
  • 工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保存它们。
  • 如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。
  • 此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。
  • Executors的newCacheThreadPool()方法创建线程池,就是使用的此种排队策略
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

 LinkedBlockingQueue

  • 无界队列策略----无界指的是工作队列大小没有上限,可以添加无数个任务进行等待。
  • 使用无界队列将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。于是创建的线程就不会超过 corePoolSize。因此,maximumPoolSize 的值也就无效了。所以一般让corePoolSize等于maximumPoolSize
  • 当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列 
  • Executors的newFixedThreadPool(int nThreads)方法创建线程池,就是使用的此种排队策略
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

ArrayBlockingQueue

  • 有界队列策略----意思是工作队列的大小是有限制的
  • 优点是可以防止资源耗尽的情况发生,因为如果工作队列被无休止的添加任务也是很危险的
  • 当工作队列排满后,就会执行线程饱和策略
// 构造线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 4, 
        3, TimeUnit.SECONDS, 
        new ArrayBlockingQueue<Runnable>(2),
        new ThreadPoolExecutor.DiscardOldestPolicy());
  • 如上核心线程为3个,每个线程的工作队列大小为2(即队列中最多有两个任务在等待执行),线程池最大线程数为4个
  • 所以当工作线程数小于等于3时,直接新建线程执行任务;超过3时,任务会被添加进工作队列进行等待,3*2=6,当工作队列等待的任务数超过6个以后,则又会新建一个线程,此时整个线程池线程总数已经达到了4个,当还有任务进行添加时,此时将采取饱和策略


相关文章: