JAVA 线程池

JAVA 线程池

背景

摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提升服务器性能的基本武器。
J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功

线程池是什么

池是一种基于池化思想管理线程的工具.

在实践中,创建对象通常是一个昂贵的操作,而池的原理就是预先创建好这些对象,在需要的时候直接从池中取出,用完后再放回,极大的节约了创建时的开销。

常用的有连接池、线程池、内存池、实例池等。

JAVA线程池是JDK中提供的ThreadPoolExecutor类。

线程池的优点:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池解决的问题

线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  • 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
  • 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  • 系统无法合理管理内部的资源分布,会降低系统的稳定性。

为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

Pooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia

线程池的设计及实现

ThreadPoolExecutor UML 图

线程池UML

ThreadPoolExecutor 运行机制

线程池运行机制

线程池的运行主要分成两部分:

  • 任务管理

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

    • 直接申请线程执行该任务;
    • 缓冲到队列中等待线程执行;
    • 拒绝该任务
  • 线程管理部分

    线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

线程的生命周期管理

线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

ctl这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。

需要先熟悉一下类中的一些重要变量,这些变量基本是是通过位运算来处理的

    private static final int COUNT_BITS = Integer.SIZE - 3;  //移位的位数
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;  //保存线程数的变量

    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

变量二进制的表现形式

the [COUNT_BITS  ] is [                              29],and binary is [                           11101]
the [CAPACITY    ] is [                       536870911],and binary is [   11111111111111111111111111111]
the [RUNNING     ] is [                      -536870912],and binary is [11100000000000000000000000000000]
the [SHUTDOWN    ] is [                               0],and binary is [                               0]
the [STOP        ] is [                       536870912],and binary is [  100000000000000000000000000000]
the [TIDYING     ] is [                      1073741824],and binary is [ 1000000000000000000000000000000]
the [TERMINATED  ] is [                      1610612736],and binary is [ 1100000000000000000000000000000]
private static int runStateOf(int c)     { return c & ~CAPACITY; } //计算当前运行状态,事实上就是取前3位状态
private static int workerCountOf(int c)  { return c & CAPACITY; }  //计算线程数,事实上就是取后29位状态
private static int ctlOf(int rs, int wc) { return rs | wc; }       //通过状态和线程数生成ctl

线程池状态
线程状态
生命周期转换:
线程生命周期

任务的执行机制

任务调度

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。
流程如下:

  • 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  • 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  • 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  • 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  • 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

执行流程如下:
线程执行流程

任务缓冲

线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

阻塞队列的工作原理:

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

阻塞队列的成员:
阻塞队列成员

任务申请

  • 任务直接由新创建的线程执行 当线程数小于coreSize时.
  • 任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。

任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
拒绝策略是一个接口,其设计如下:

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

用户可以通过实现这个接口去定制拒绝策略,也可以选择JDK提供的四种已有拒绝策略,JDK
提供的策略如下:

拒绝策略

实际使用中的问题

线程数设置的问题:

corePoolSize,maximumPoolSize这两个参数的设置在很大程度上强依赖开发人员的经验和知识,没有通用的方案,但在大的方向上有一个参考方案:主要分为IO密集型和CPU密集型,这两种任务尽量分别配置

  • IO密集型
    由于线程并不是一直在执行任务,大部分处理等待状态,这时候应该根据业务尽可能配置多的线程
  • CPU 密集型
    由于线程一直在执行任务,这时候尽量避免上下文切换,尽可能配置少的线程,推荐CPU数+1

阻塞队列选择的问题

  • 要求实时响应快的应用

    这时候要可以选择使用SynchronousQueue队列,并配置设置比较大corePoolSize和maximumPoolSize以避免频繁创建新线程,以便及时处理请求.或者采用比较小的有界队列,避免长时间的等待

  • 要求吞吐量的应用 可以选择比较大的有界队列,尽量避免不要选用无界队列,以致把内存吃完.

参考文章
美团

posted @ 2020-08-05 16:36  xjzcz  阅读(101)  评论(0编辑  收藏  举报