JUC高级多线程_08:线程池的具体介绍与使用

Posted ABin-阿斌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC高级多线程_08:线程池的具体介绍与使用相关的知识,希望对你有一定的参考价值。

我是 ABin-阿斌:写一生代码,创一世佳话,筑一揽芳华。 如果小伙伴们觉得我的文章有点 feel ,那就点个赞再走哦。
在这里插入图片描述

1 . 简介

1. 线程池的特点:

  • 线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列然后在线程创建后启动这些任务如果线程数量 超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
  • 主要特点: 线程复用、控制最大并发数、管理线程

2. 线程池的优点:

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

2 . 使用

1. 架构

  • Java 中的线程池是通过 Executor 框架实现的,该框架中用到了ExecutorExecutorsExecutorServiceThreadPoolExecutor 这几个类,主要使用 ExecutorService 接口

2. 线程池的三大方法:

  • Executors.newFixedThreadPool(int)

    • 是用于执行长期任务性能好,有固定的线程数,创建一个线程池,一池有N个固定的线程
  • Executors.newSingleThreadExecutor()

    • 一个任务一个任务的执行,一池一线程
  • Executors.newCachedThreadPool()

    • 执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强
public class JUC10_ThreadPool {
    public static void main(String[] args) {
        // 1. 一 个线程池受理 五 个线程
//        ExecutorService threadpool = Executors.newFixedThreadPool(5);
        // 2. 一 个线程池受理 一 个线程
//        ExecutorService threadpool = Executors.newSingleThreadExecutor();
        // 3. 一 个线程池受理 N 个线程,可扩容
        ExecutorService threadpool = Executors.newCachedThreadPool();

        try {
            //模拟有 10 个顾客来办理业务,但只有 5 个办理窗口
            for (int i = 1; i <= 10; i++) {
                threadpool.execute(()->{
                    System.out.println("当前"+ Thread.currentThread().getName()+"办理业务");
                });


                //模拟办理所需时间
//                try {
//                    TimeUnit.MILLISECONDS.sleep(100);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadpool.shutdown();
        }
    }
}

3 . 底层源码

  • 由图可知上述三个 API 底层调用的都是同一个方法 —— threadPoolExecutor
  • 第五个参数表示一个阻塞队列:
    • LinkedBlockingQueue: 由链表结构组成的有界(但大小默认值为 integer.MAX_VALUE)阻塞队列。
    • SynchronousQueue: 不存储元素的阻塞队列,也即单个元素的队列。(生产一个,消费一个)

4 . threadPoolExecutor 7 个重要参数

1. int corePoolSize:(核心线程池大小)

  • 线程池中的常驻核心线程数,最少存在的线程数

2. int maximumPoolSize:(最大核心线程池大小)

  • 线程池中能够容纳同时执行的最大线程数,此值必须大于等于1

3. long keepAliveTime:(超时了没有人调用就会释放)

  • 多余的空闲线程的存活时间
  • 当前池中线程数量超过 corePoolSize 时,且当空闲时间达到 keepAliveTime 时,多余线程会被销毁直到只剩下 corePoolSize 个线程为止

4. TimeUnit unit:(超时单位)

  • keepAliveTime 的单位 —— 秒 / 毫秒 / 微秒

5. BlockingQueue workQueue:(阻塞队列)

  • 任务队列,被提交但尚未被执行的任务

6. threadFactory threadFactory:(线程工厂:创建线程的,一般 不用动)

  • 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可

7. RejectedExecutionHandler handler:(拒绝策略)

  • 拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝
    请求执行的 runnable 的策略

5 . 底层原理

  • 在创建了线程池后,开始等待请求。
  • 当调用 execute() 方法添加一个请求任务时,线程池会做出如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入 workQueue队列
    • 如果这个时候 workQueue队列 满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行 workQueue队列 中的任务;
    • 如果 workQueue队列 满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会 启动饱和拒绝策略来执行
  • 当一个线程完成任务时,它会从 workQueue队列 中取下一个任务来执行。
  • 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
    • 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
    • 所以线程池的所它最终会收缩到 corePoolSize 的大小。

6 . 线程池的选择

  • 在工作中单一的、固定数的、可变的,三种创建线程池的方法哪个用的多?及为什么?
  • 答 : 一个都不用
  • 我们可以看看 《阿里巴巴手册》 给我们的建议:

7 . 自定义线程池

1. 代码示例

public static void main(String[] args) {
        ExecutorService threadpool = new ThreadPoolExecutor(2,5, 2L,TimeUnit.SECONDS,
                                                            new LinkedBlockingQueue<>(3),
                                                            Executors.defaultThreadFactory(),
                                                            new ThreadPoolExecutor.AbortPolicy());

        try {
        	// 改变任务数,观察输出结果
            //模拟有 N 个顾客来办理业务
            for (int i = 1; i <= 5; i++) {
                threadpool.execute(()->{
                    System.out.println("当前\\t"+ Thread.currentThread().getName()+"\\t办理业务");
                });
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadpool.shutdown();
        }
    }

  • 任务队列(workQueue)的参数如果不写就默认为 Integer.MAX_VALUE,所以需要写上

  • 线程工厂使用默认的,模仿源码

  • 拒绝策略也暂时使用和默认的,下文具体阐述

2. 改变任务的数量

  • 情况一:5 个任务

    • 不会报错,正常执行,两个线程处理完成
  • 情况二:8 个任务

    • 不会报错,正常执行,五个线程处理完成
  • 情况三:9 个任务

    • 会报错,会报 RejectedExecutionException
  • 总结:

    • 线程池的最多可以容纳数 =【最大线程数(maximumPoolSize) + 任务队列(workQueue)可容纳数】

3. 最大线程数的设置规则

  • 如果是 CPU 密集型(CPU 用的最多):maximumPoolSize(最大线程数) = CPU核数 + 1

  • 注意:在实际开发当中写死参数的方法不可取,原因:每个人的电脑核数不一样。

  • 具体优化方案如下代码展示: 采取自动获取 CPU 核数

// 获取电脑的 CPU 核数
int CPU = Runtime.getRuntime().availableProcessors();
int maximumPoolSize = CPU + 1;
  • 如果是 IO 密集型 : maximumPoolSize(最大线程数)= CPU核数 / 阻塞系数

8 .拒绝策略(4种)

1. 简介

  • 等待队列已经排满了,再也塞不下新任务了,同时,线程池中的 max 线程也达到了,无法继续为新任务服务
  • 也即 最多可以容纳数 达到最大
  • 这个是时候我们就需要拒绝策略机制合理的处理这个问题

2. 分类(4 种)

  • ThreadPoolExecutor.AbortPolicy()

    • 解释 : 当线程池中任务数量超出 最多可容纳数 时,会直接抛出 RejectedExecutionException 异常 阻止系统正常运行
  • Executor 默认的策略

  • ThreadPoolExecutor.CallerRunsPolicy

    • 调用者运行 一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
  • 举例 :

    • 营业厅A 人多,告诉顾客去 营业厅B 办理 ,可是顾客来到 营业厅B 时, 营业厅B 人更多,便告诉顾客再去 营业厅A 看看。 —— 即不放弃任何一个顾客 ; 营业厅B 把顾客 回退营业厅A
  • ThreadPoolExecutor.DiscardOldestPolicy

    • 抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
  • ThreadPoolExecutor.DiscardPolicy

    • 该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。

以上是关于JUC高级多线程_08:线程池的具体介绍与使用的主要内容,如果未能解决你的问题,请参考以下文章

JUC高级多线程_10:Future异步回调的具体介绍与使用

JUC高级多线程_07:读写锁与阻塞队列的具体介绍与使用

JUC高级多线程_11:JMM与Volatile的具体介绍与使用

JUC高级多线程_04:高并发下集合类的具体使用

JUC高级多线程_06:多线程下得常用辅助类

Java---JUC并发篇(多线程详细版)