• Java基础系列(八)——线程池详解


    目录

    线程池详解

    什么是线程池?为什么要用线程池?

    如何创建线程池

    ThreadPoolExecutor类分析

    ThreadPoolExecutor 饱和策略

    简单的线程池Demo

    ThreadPoolExecutor线程池新增线程流程


    线程池详解

    什么是线程池?为什么要用线程池?

    处理高并发场景或者是经常连接数据库的小伙伴应该经常听到或者用过“池”,那么什么是线程池呢?为什么要用线程池呢?

    举个简单的例子:

    从前,有个店铺,每天的客户都很少,所以,不需要一个常驻的收银员,都是来一个客户找个临时工充当收银员,结束后临时工就被辞退,每次都是招聘与解雇循环往复。当客户量比较少的时候还可以处理的过来(单线程模式),过几年之后,店铺越做越大,客户越来越多,这种模式就已经处理不过来增长的用户量了(高并发场景)。此时,常驻收银员就出现了,而且可能一个常驻还不够,需要好几个。这个时候我们就可以将这一堆的常驻收银员看成是一个"池",每来一个客户就处理业务,没有客户就进行等待,省去了重复招聘与解雇的时间了。

    池化技术的思想主要是为了减少每次获取资源的消耗提升对资源的利用率。线程池提供了一种限制和管理资源(包括执行一个任务)的方式。每一个线程池还维护一些基本统计信息,例如已完成任务的数量。

    在《Java并发编程的艺术》提到的使用线程池的好处:

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

    如何创建线程池

    在《阿里巴巴Java开发手册》中提到强制线程池中不允许使用Executor去创建,而是通过ThreadPoolExecutor的方式,这样可以更加灵活去使用线程池,规避资源耗尽的风险。

    Executors 返回线程池对象的弊端如下:

    • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
    • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

    方法1:使用ThreadPoolExecutor的构造方法实现

     后面会对于这个类进行详细分析。

    方法2:通过Executor框架的工具类Executors来实现

    我们可以创建三种类型的 ThreadPoolExecutor

    • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
    • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
    • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

    直接看看源码。

     可以看源码发现,Executor框架底层都是使用ThreadPoolExecutor进行实现,并且确实如《阿里巴巴Java开发手册》所说,默认的值都是给到了integer的最大值,这样相当于埋下雷,指不定哪天就OOM了……

    ThreadPoolExecutor类分析

    ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么)。

     参数分析:

    ThreadPoolExecutor 3 个最重要的参数:

    • corePoolSize : 核心线程数定义了最小可以同时运行的线程数量。
    • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
    • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
      • ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。

      • LinkedBlockingQuene:基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。

      • SynchronousQuene:一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。

      • PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级通过参数Comparator实现。

    ThreadPoolExecutor其他常见参数:

    1. keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
    2. unit : keepAliveTime 参数的时间单位。
    3. threadFactory :创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。
    4. handler :饱和策略。关于饱和策略下面单独介绍一下。

    ThreadPoolExecutor 饱和策略

    ThreadPoolExecutor 饱和策略定义:

    如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor 定义一些策略:

    • ThreadPoolExecutor.AbortPolicy 抛出 RejectedExecutionException来拒绝新任务的处理。
    • ThreadPoolExecutor.CallerRunsPolicy 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
    • ThreadPoolExecutor.DiscardPolicy 不处理新任务,直接丢弃掉。
    • ThreadPoolExecutor.DiscardOldestPolicy 此策略将丢弃最早的未处理的任务请求。

    举个例子: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)

    简单的线程池Demo

    创建一个Runnable接口的实现类

    1. import java.util.Date;
    2. /**
    3. * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
    4. */
    5. public class MyRunnable implements Runnable {
    6. private String command;
    7. public MyRunnable(String s) {
    8. this.command = s;
    9. }
    10. @Override
    11. public void run() {
    12. System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
    13. processCommand();
    14. System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
    15. }
    16. private void processCommand() {
    17. try {
    18. Thread.sleep(5000);
    19. } catch (InterruptedException e) {
    20. e.printStackTrace();
    21. }
    22. }
    23. @Override
    24. public String toString() {
    25. return this.command;
    26. }
    27. }

    使用推荐的ThreadPoolExecutor构造函数自定义参数的方式创建线程池。

    1. import java.util.concurrent.ArrayBlockingQueue;
    2. import java.util.concurrent.ThreadPoolExecutor;
    3. import java.util.concurrent.TimeUnit;
    4. public class ThreadPoolExecutorDemo {
    5. private static final int CORE_POOL_SIZE = 5;
    6. private static final int MAX_POOL_SIZE = 10;
    7. private static final int QUEUE_CAPACITY = 100;
    8. private static final Long KEEP_ALIVE_TIME = 1L;
    9. public static void main(String[] args) {
    10. //使用阿里巴巴推荐的创建线程池的方式
    11. //通过ThreadPoolExecutor构造函数自定义参数创建
    12. ThreadPoolExecutor executor = new ThreadPoolExecutor(
    13. CORE_POOL_SIZE,
    14. MAX_POOL_SIZE,
    15. KEEP_ALIVE_TIME,
    16. TimeUnit.SECONDS,
    17. new ArrayBlockingQueue<>(QUEUE_CAPACITY),
    18. new ThreadPoolExecutor.CallerRunsPolicy());
    19. for (int i = 0; i < 10; i++) {
    20. //创建 MyRunnable 对象(MyRunnable 类实现了Runnable 接口)
    21. Runnable worker = new MyRunnable("" + i);
    22. //执行Runnable
    23. executor.execute(worker);
    24. }
    25. //终止线程池
    26. executor.shutdown();
    27. while (!executor.isTerminated()) {
    28. }
    29. System.out.println("Finished all threads");
    30. }
    31. }

    参数:

    1. corePoolSize: 核心线程数为 5。
    2. maximumPoolSize :最大线程数 10
    3. keepAliveTime : 等待时间为 1L。
    4. unit: 等待时间的单位为 TimeUnit.SECONDS。
    5. workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
    6. handler:饱和策略为 CallerRunsPolicy

    output:

    1. pool-1-thread-3 Start. Time = Sun Apr 12 11:14:37 CST 2020
    2. pool-1-thread-5 Start. Time = Sun Apr 12 11:14:37 CST 2020
    3. pool-1-thread-2 Start. Time = Sun Apr 12 11:14:37 CST 2020
    4. pool-1-thread-1 Start. Time = Sun Apr 12 11:14:37 CST 2020
    5. pool-1-thread-4 Start. Time = Sun Apr 12 11:14:37 CST 2020
    6. pool-1-thread-3 End. Time = Sun Apr 12 11:14:42 CST 2020
    7. pool-1-thread-4 End. Time = Sun Apr 12 11:14:42 CST 2020
    8. pool-1-thread-1 End. Time = Sun Apr 12 11:14:42 CST 2020
    9. pool-1-thread-5 End. Time = Sun Apr 12 11:14:42 CST 2020
    10. pool-1-thread-1 Start. Time = Sun Apr 12 11:14:42 CST 2020
    11. pool-1-thread-2 End. Time = Sun Apr 12 11:14:42 CST 2020
    12. pool-1-thread-5 Start. Time = Sun Apr 12 11:14:42 CST 2020
    13. pool-1-thread-4 Start. Time = Sun Apr 12 11:14:42 CST 2020
    14. pool-1-thread-3 Start. Time = Sun Apr 12 11:14:42 CST 2020
    15. pool-1-thread-2 Start. Time = Sun Apr 12 11:14:42 CST 2020
    16. pool-1-thread-1 End. Time = Sun Apr 12 11:14:47 CST 2020
    17. pool-1-thread-4 End. Time = Sun Apr 12 11:14:47 CST 2020
    18. pool-1-thread-5 End. Time = Sun Apr 12 11:14:47 CST 2020
    19. pool-1-thread-3 End. Time = Sun Apr 12 11:14:47 CST 2020
    20. pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020

    分析

    承接 4.6 节,我们通过代码输出结果可以看出:线程池首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。

    现在,我们就分析上面的输出内容来简单分析一下线程池原理。

    为了搞懂线程池的原理,我们需要首先分析一下 execute方法。 在Demo 中我们使用 executor.execute(worker)来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:

    1. // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
    2. private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    3. private static int workerCountOf(int c) {
    4. return c & CAPACITY;
    5. }
    6. private final BlockingQueue workQueue;
    7. public void execute(Runnable command) {
    8. // 如果任务为null,则抛出异常。
    9. if (command == null)
    10. throw new NullPointerException();
    11. // ctl 中保存的线程池当前的一些状态信息
    12. int c = ctl.get();
    13. // 下面会涉及到 3 步 操作
    14. // 1.首先判断当前线程池中执行的任务数量是否小于 corePoolSize
    15. // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
    16. if (workerCountOf(c) < corePoolSize) {
    17. if (addWorker(command, true))
    18. return;
    19. c = ctl.get();
    20. }
    21. // 2.如果当前执行的任务数量大于等于 corePoolSize 的时候就会走到这里
    22. // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态并且队列可以加入任务,该任务才会被加入进去
    23. if (isRunning(c) && workQueue.offer(command)) {
    24. int recheck = ctl.get();
    25. // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
    26. if (!isRunning(recheck) && remove(command))
    27. reject(command);
    28. // 如果当前线程池为空就新创建一个线程并执行。
    29. else if (workerCountOf(recheck) == 0)
    30. addWorker(null, false);
    31. }
    32. //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
    33. //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
    34. else if (!addWorker(command, false))
    35. reject(command);
    36. }

    流程如图:

    流程:

    我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的5个任务中如果有任务被执行完了,线程池就会去拿新的任务执行。

    ThreadPoolExecutor线程池新增线程流程

    从注释可以看到这个HashSet是存放所有的工作线程的容器,也就是线程池最核心的容器。我们看到ThreadPoolExecutor的构造函数中并没有对workers进行添加操作。只是对于变量进行了一个赋值操作,也就是说在ThreadPoolExecutor被new出来后workers容器里面是空的,所以线程池创建时内部可用线程数为0。那什么时候才创建了线程放在线程池中?我们知道提交任务无非两种方式execute和submit。

    这里简单说下执行execute和submit的区别:

    1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
    2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
    3. 线程池针对不同的提交方式会抛出堆栈异常,execute方法会抛出异常而submit则不会。出现异常不会影响其他线程任务的执行,最后该异常线程会被清理,线程池会重新添加一个新线程作为补充。

    源码展示下:

    execute在ThreadPoolExecutor中

    其中三个if的判断逻辑:

    • 如果运行的线程少于corePoolSize,尝试以给定的命令作为第一个启动新线程

    • 如果一个任务可以成功地排队,那么我们仍然需要再次检查是否应该添加线程,还是应该在进入此方法后关闭池。

    • 如果不能对任务进行排队,则尝试添加一个新线程。

    submit在抽象类AbstractExecutorService中

     其实可以看到,submit的底层还是调用的是execute方法。

     回归正题,新增线程的过程。

    可以看看debug的过程

    其实,就是调用了addWorker方法的时候,如果成功就会在之前的hashSet中插入一个work,可以看看下面addWorker源码。

     

  • 相关阅读:
    计算机设计大赛 深度学习猫狗分类 - python opencv cnn
    X11 Xlib截屏问题及深入分析二 —— 源码实现1
    在Pycharm不同项目中使用同一环境
    如何为谷歌seo打好基础?
    【JVM系列】- 探索·运行时数据区的私有结构
    JavaScript_notes
    zookeeper选举机制
    前端对接阿里oss保姆级教程(第二章使用武器)
    节后面试必备:Spring 面试63问
    Mysql 索引原理和优化方式
  • 原文地址:https://blog.csdn.net/Stray_Lambs/article/details/127771708