[Java高并发系列(5)][详细]Java中线程池(1)--基本概念介绍

1 Java中线程池概述

1.1 什么是线程池?

在一个应用当中, 我们往往需要多次使用线程, 这意味着我们需要多次创建和销毁线程.那么为什么不提供一个机制或概念来管理这些线程呢? 该创建的时候创建, 能复用的时候复用, 何乐而不为呢? Java 中开辟了一种管理线程的概念 , 即为线程池.

1.2 为什么要使用线程池?

那么究竟线程池有啥好处? 为啥要用它呢?

  1. 线程创建所需时间为T1,线程执行任务时间为T2,线程销毁时间为T3,而往往T1+T3>T2。所以频繁创建线程会损坏而外的时间。
  2. 如果有任务来了,再去创建线程的话效率比较低。
  3. 线程池可以管理控制线程,线程是稀缺资源,如果无休止的创建会消耗系统资源,还会降低系统稳定性。使用线程池可以进行统一分配,方便调优和监控。
  4. 线程池提供队列,存放缓冲等待执行任务。

具象来说, 线程池可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

由此可见 , 线程池在时间, 空间和效率上对程序运行都有好处, 那么为何不一起来学学呢?

1.3 线程池作用

承接线程池的好处, 线程的作用也可以总结为 控制系统中线程的数量, 合理调配线程执行任务.

具体来说:

  1. 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果。
  2. 少了浪费了系统资源,多了造成系统拥挤效率不高。
  3. 用线程池控制线程数量,其他线程排 队等候. (线程池需要使用到队列? YES)
  4. 一个任务执行完毕,再从队列的中取最前面的任务开始执行。
  5. 若队列中没有等待进程,线程池的这一资源处于等待。

2 Java中使用线程池

2.1 Java中线程池的一些基本接口和类

在这里插入图片描述

Executor

线程池的顶级接口

ExecutorService
真正的线程池接口

ScheduledExecutorService
能和Timer/TimerTask类似,解决那些需要任务重复执行的问题

AbstractExecutorService 抽象类

ThreadPoolExecutor

是ExecutorService的默认实现,也是我们重点讨论的部分

ScheduledThreadPoolExecutor

继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现

Executors

线程池中的工具类 , 类比于集合类中的Connections之类的

下面我们来看看ThreadPoolExecutor是如何创建线程池的

2.2 使用 ThreadPoolExecutor 构造线程池

ThreadPoolExecutor是线程池的真正实现,他通过构造方法的一系列参数,来构成不同配置的线程池。常用的构造方法有四个, 但其中最重要(全面)的一个如下:

public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)

这个构造方法确定了创建的线程池的各种属性, 我们通过介绍其中各参数来介绍与之相关概念. 在下一篇文章中, 再详细介绍各种参数对应的各种线程池及其使用.

下面一张图来体会线程池和这几个参数:

在这里插入图片描述

由上图: 我们看到 corePoolSize 就是线程池中的核心线程数量, 这几个核心线程, 在没有用的时候也不会被回收; maximumPoolSize就是线程池中可以容纳的最大线程数量; 而keepAliveTime , 就是线程池中除核心线程的非核心线程的最长可存活时间, 因为在线程池汇总, 除了核心线程即使在无任务的时候也不能清除 , 其余的都是有存活时间的, 意思就是非核心线程可以存活的最长时间; 而util就是计算这个时间的一个单位; 图上方的 等待执行的任务队列 workQueue , 即等待队列, 任务可以存储在任务队列中等待被执行, 执行的原则是 FIFO ; threadFactory,就是创建线程的线程工厂; 最后一个handler,是一种拒绝策略,我们可以在任务满了之后 , 拒绝执行某些任务 .

下面分别介绍构造方法参数:

  • corePoolSize

    核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。 (如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程)

  • maximumPoolSize

    线程池所能容纳的最大线程数 ( maximumPoolSize肯定是大于等于corePoolSize)。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。

  • keepAliveTime

    非核心线程的闲置超时时间,超过这个时间就会被回收。

  • unit

    指定keepAliveTime的单位,如 TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。

  • workQueue

    线程池中的任务队列. 常用的有三种队列,SynchronousQueue, LinkedBlockingDeque, ArrayBlockingQueue (下文会详细介绍这几个任务队列)

threadFactory

线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法

public interface ThreadFactory {
  Thread new Thread(Runnable r);
}
  • RejectedExecutionHandler

    当线程边界和队列容量已经达到最大时,用于处理阻塞时的程序 , RejectedExecutionHandler也是一个接口,只有一个方法

public interface RejectedExecutionHandler {
  void rejectedExecution(Runnable var1, ThreadPoolExecutor var2);
}

2.3 线程池的执行流程

看到这, 又是核心又是非核心的 , 还有存活时间 , 任务队列,拒绝啥的, 如果初次接触线程池可能都晕了, 下面我们来看看线程池是如果执行的 , 这些个概念也就能连接起来.

在这里插入图片描述

通过图, 我们再用文字梳理 下:任务进来时, 首先执行判断, 判断核心线程是否处于满员/ 不可用 状态,

  • 如果核心线程未达上限 , 核心线程就先就执行任务;
  • 如果核心线程已满 ; 则判断任务队列是否有地方存放该任务,
    • 如果任务队列未满,就将任务保存在任务队列中,等待执行;
    • 如果任务队列满了, 再判断最大可容纳的线程数,
      • 如果小于最大可容纳线程数, 就开创非核心线程执行任务.
      • 如果已经达到最大线程数,就调用handler实现拒绝策略

2.4 三种任务队列

在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务, 根据workQueue的不同性质, 分为以下三种

workQueue的类型为BlockingQueue,通常可以取下面三种类型:

1)有界任务队列ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

2)无界任务队列LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

3)直接提交队列synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务.

2.5 四种拒绝策略

拒绝策略是在核心线程数量达到最大限度后 , 队列也满了, 非核心也最大数量了 , 拒绝当前任务的策略, 主要分为以下四种:

  1. AbortPolicy:丢弃任务并抛出RejectedExecutionException

  2. CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

  3. DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

  4. DiscardPolicy:丢弃任务,不做任何处理。

2.6 五种线程池

根据上述的任务队列类型, 以及相应的参数的搭配 , Java抽离出线程池常用的五种类型五种类型

  1. CachedThreadPool

    可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

    特征:
    (1)线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
    (2)线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)
    (3)当线程池中,没有可用线程,会重新创建一个线程

  2. FixedThreadPool

    定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程

    特征:
    (1)线程池中的线程处于一定的量,可以很好的控制线程的并发量
    (2)线程可以重复被使用,在显示关闭之前,都将一直存在
    (3)超出一定量的线程被提交时候需在队列中等待

  3. SingleThreadExecutor

    一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程, 他只有一条线程来执行任务,适用于有顺序的任务的应用场景

    特征:
    (1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行

  4. ScheduleThreadPool

    创建一个线程池, 它可安排在给定延迟后运行命令或定期地执行.

    周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。

    特征:
    (1)线程池中具有指定数量的线程,即便是空线程也将保留
    (2)可定时或者延迟执行线程活动

  5. newSingleThreadScheduledExecutor

    作用: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。

    特征:
    (1)线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行
    (2)可定时或者延迟执行线程活动

2.6 线程池执行任务

最核心的execute方法,这个方法在AbstractExecutorService中并没有实现,从Executor接口,直到ThreadPoolExecutor才实现了改方法,

ExecutorService中的submit(),invokeAll(),invokeAny()都是调用的execute方法,所以execute是核心中的核心,源码分析将围绕它逐步展开。

2.7 线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务.

本文是线程池系列的第一篇, 主要介绍一些java线程池中的概念, 直观感受线程池的创建和工作流程.

在接下来的文章中, 我们将讨论线程如何包装自己得使得线程池来执行他, 换句话说, 线程池究竟是怎样执行线程们的? 是不是new 一个 runnable丢到线程池中 (即详细讨论Executor, execute方法, runnable , callable , 以及其返回类型future之类的)
之后将详细介绍这ExecutorService以及其ThreadPoolExecutor,包括这几种几种线程池的使用.

[Java高并发系列(1)]Java 中 synchronized 关键字详解 + 死锁实例
[Java高并发系列(2)]Java 中 volatile 关键字详解 + volatile 与 sychronized 区别
[Java高并发系列(3)]Java 中 CountDownLatch介绍 + 一道面试题
[Java高并发系列(4)]Java 中 ReentrantLock 介绍 + 一道面试题

发布了47 篇原创文章 · 获赞 108 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/Lagrantaylor/article/details/103346995