前言

线程是稀缺的资源,它的创建与销毁是比较消耗资源的操作。而java线程是依赖于内核线程,创建线程需要进行操作系统的状态切换,为了避免资源过度消耗需要设法重用线程执行多个任务。线程池就是一个线程缓存,负责对线程进行统一分配、调优和监控。

线程池的优势

  1. 重用存在的线程,减少线程创建,消亡的开销,提高性能
  2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限创建不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以对线程进行统一分配、调优和监控。
  4. 提供更强大的功能,例如延时定时功能

Executor框架

java中线程池使用 java中线程池的作用_阻塞队列


ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需要提供Runnable对象,将任务的运行逻辑提交到执行器Executor中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力

  1. 扩充执行任务的能力,补充可以为一个或者一批异步任务生成Future的方法;
  2. 提供了管控线程池的方法,比如停止线程池的运行。
    AbstractExecutorService则是上层的抽象类,将执行任务的流程串联起来,保证下层的实现只需要关注一个执行任务的方法即可。
    最下层的ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

ThreadPoolExecutor运行流程

java中线程池使用 java中线程池的作用_java中线程池使用_02


线程池的内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而有利于缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理,线程管理。

任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务的后续的流转:

  1. 直接申请线程执行该任务
  2. 缓冲到队列中等待线程执行
  3. 拒绝该任务
    线程管理部分是消费者,他们被统一维护在线程池内,根据任务的请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行。

创建线程池

  • 通过Executors方式创建
    Executors是线程池辅助工具类,一般创建简单的线程池都是通过Executors的静态方法创建。
  • newFixedThreadPool(int nThreads),创建固定大小的线程池,核心数和最大数是一样的。
  • newSingleThreadExecutor(),创建一个单线程的线程池。这个线程池的核心数和最大数都是1,也就是相当于单线程串行执行所有任务。
  • newCachedThreadPool(),创建一个可缓存的线程池。核心数是0,最大数是Integer.MAX_VALUE,60秒不执行任务就回收。
  • newScheduledThreadPool(int corePoolSize)
  • newWorkStealingPool(),jdk1.8新增加的创建线程池的方式,forkJoinPool可以根据CPU的核数并行的执行,适合使用在很耗时的操作,可以充分利用CPU执行任务,任务不保证执行顺序。
    线程池中有多个线程队列,有的线程队列中有大量的比较耗时的任务堆积,而有的线程队列却是空的,就存在有的线程处于饥饿状态,当一个线程处于饥饿状态时,就会去其他的线程队列中窃取任务。解决饥饿导致的效率问题。
    注意:不建议使用Executors去创建线程池,而是通过ThreadPoolExecutor的方式,这样的处理方式能够更加明确线程池的运行规则,规避资源耗尽的风险。
    Executors返回的线程池对象的弊端如下:
  • FixedThreadPool和SingleThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,导致OOM。
  • CachedThreadPool和ScheduledThreadPool允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
  1. 通过ThreadPoolExecutor创建线程池
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
  • corePoolSize,核心线程数(线程池基本大小):当向线程池提交一个任务,若线程池已经创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或者等于corePoolSize(除了利用提交新的任务来创建和启动线程,也可以通过prestartCoreThread()或者prestartAllCoreThreads()方法来提前启动线程池中的基本线程)。
  • maximumPoolSize,最大线程数(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外对于无界队列,则忽略该参数。
  • keepAliveTime,存活时间(线程存活保持时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
  • unit,时间单位(存活时间的单位);
  • workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。常用的队列:
    ArrayBlockingQueue(有界队列):这是一个由数组实现的容量固定的有界阻塞队列。
    SynchronousQueue(同步移交队列):没有容量,不能缓存数据,每个put必须等待一个take,offer()的时候如果没有另一个线程在poll()或者take()的话返回false。
    LinkedBlockingQueue(无界队列):这是一个由单链表实现的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。
  • threadFactory(线程工厂):用于创建新的线程。ThreadFactory创建的线程也是采用new Thread()方式,ThreadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
  • handler(线程饱和策略):当线程和队列都满了,再加入线程会执行此策略。
    AbortPolicy:中断并抛出异常
    DiscardPolicy:默默丢弃任务,不进行任何通知
    DiscardOLdestPolicy:丢弃掉在队列中存在时间最久的任务
    CallerRunsPolicy:让提交任务的线程去执行任务。

线程池的状态

  1. Running:能接收新任务,以及处理已经添加的任务
  2. Shutdown:不接受新任务,可以处理已经添加的任务
  3. Stopp:不接收新任务,不处理已经添加的任务,并且中断正在处理的任务。
  4. Tidying:所有的任务已经终止,ctl记录的任务数量为0(ctl负责记录线程池的运行态和活动线程数)
  5. Terminated:线程池彻底终止,则线程池转化为terminated状态。
    这几种状态的转换过程:

线程池的工作流程

  1. 任务调度
    任务调度是线程池的主要入口,当用户提交一个任务,接下来这个任务将如何执行都是由这个阶段决定的。
    所有的任务调度都是由execute方法完成的,通过检查线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或者缓冲到队列中执行,亦或是直接拒绝该任务。
  2. java中线程池使用 java中线程池的作用_java中线程池使用_03

  3. 任务缓冲
    任务缓冲是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的,阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
    阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
  4. 任务申请
    由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。
6. 任务拒绝
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略是一个接口,其设计如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

execute和submit对比

execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。

shutdown()和shutdownNow()是用来关闭线程池的。