并发编程 - 线程池

目录

一、什么是线程池

1.1、什么是线程池

1.2、为什么用线程池

二、JDK自带的构建线程池方式

2.1 newFixedThreadPool

2.2 newSingleThreadExecutor

2.3 newCachedThreadPool

2.4 newScheduleThreadPool

2.5 newWordStealingPool

ThreadPoolExecutor的核心点:

 ForkJoinPool的核心特点:

 举例:来一个比较大的数组,里面存满值,计算总和 :

三、(重点)ThreadPoolExecutor

3.1 为什么要自定义线程池

3.2 ThreadPoolExecutor中的7个参数

3.3 ThreadPoolExecutor应用

3.3.1 JDK提供的四种拒绝策略(RejectedExecutionHandler)

3.3.1.1 AbortPolicy(默认)

3.3.1.2 CallerRunsPolicy

3.3.1.3 DiscardPolicy

3.3.1.4  DIscardOldestPolicy

 3.3.1.5 如何设置(使用)拒绝策略

 3.3.1.6 选择拒绝策略的一些建议

 3.4 (重点)ThreadPoolExecutor 源码分析

四、线程池的核心参数设计规则

五、线程池处理任务的核心流程


一、什么是线程池

1.1、什么是线程池

线程池的核心思想是“线程复用”,通过维护一个线程集合(池)和任务队列,将任务提交与线程调度解耦。

核心组件

1、线程集合:预先创建核心线程与非核心线程

        核心线程:线程可以长期保留。

        非核心线程:设置一个时间,当没有任务需要线程处理时,超时后线程就会被销毁。

2、任务队列:存储待执行的任务(一般使用阻塞队列),避免任务堆积时直接创建过多的线程。

3、拒绝策略:当任务超出处理能力时,定义如何处理新任务(丢弃、抛出异常....)。

1.2、为什么用线程池

1、降低资源开销

  • 线程创建/销毁成本高
    • 操作系统创建线程涉及内存分配、上下文切换等操作,频繁操作会消耗大量的资源
  • 线程池复用线程
    • 通过复用已有的线程,减少系统开销,提升响应速度

2、控制并发规模

  • 防止资源耗尽
    • 通过限制最大线程数,避免无限制创建线程导致内存或CPU过载(”线程爆炸“问题)
  • 任务队列缓冲
    • 当任务激增时,队列作为缓冲区平增流量,避免系统瞬间崩溃

3、提升可管理性

  • 统一监控与调优
    • 可统计线程执行情况、调整核心参数(后面在ThreadPoolExecutor中会详细介绍的),优化系统功能
  • 灵活的任务调度
    • 支持定时任务、优先级任务等扩展功能(如ScheduleThreadPoolExecutor)

二、JDK自带的构建线程池方式

JDK中基于Executor提供了很多种线程池

2.1 newFixedThreadPool

这个线程池的特点是:线程数是固定的。

  • 在构建时,需要给newFixedThreadPool方法提供一个nThread参数。这个参数就是当前线程池种的个数,当前线程池的不呢之其实就是使用ThreadPoolExecutor。
  • 构建好当前线程池后,线程个数已经固定好(线程是懒加载,在构建之初,线程并没有构建出来,而是随着任务的提交才会将线程在线程池中构建出来)。如果线程没构建,线程会待着任务执行被创建和执行。如果线程都已经构建好了,此时任务会被放到LinkedBlockingQueue无界队列中存放,等待线程从LinkedBlockingQueue中去take出任务,然后执行。

2.2 newSingleThreadExecutor

这个线程池是单例线程池,线程池种只有一个工作线程在处理任务。

使用场景:如果涉及到顺序消费可以使用这个

  • 在内部依然是构建了ThreadPoolExecutor,设置的线程个数为1
  • 当任务投递过来后,第一个任务会被工作线程处理,后续的任务会被扔到阻塞队列中
  • 投递到阻塞队列中任务的顺序,就是工作线程处理的顺序
  • 当前这种线程池可以用作顺序处理的一些业务中

如果是局部变量仅限当前线程池使用的线程池,在使用完毕之后要记得执行shutdown,避免线程无法结束  

2.3 newCachedThreadPool

这个线程池的最大线程数Integer的最大数。

1、当第一次提交任务到线程池时,会直接构建一个工作线程,

2、这个工作线程等任务执行完后,60秒米有任务可以执行,会结束

3、如果在等待期间(60S)内有任务来,他会再次拿到这个任务去执行

4、如果后续有任务提交,但是没有线程是空闲的,那么就会构建工作线程去执行。

最大特点:任务只要提交给当前的线程池,就必然会有工作线程可以处理

2.4 newScheduleThreadPool

定时任务的线程池,而这个线程池就是可以以一定周期去执行一个任务,或者是延迟多久执行一个任务一次

原理是基于DelayQueue实现的延迟执行。周期性执行是任务执行完毕后,再次扔回到阻塞队列。

2.5 newWordStealingPool

  • 当前JDK提供构建线程池的方式newWorkStealingPool和之前的线程池很非常大的区别
  • 之前定长,单例,缓存,定时任务都基于ThreadPoolExecutor去实现的。
  • newWorkStealingPool是基于ForkJoinPool构建出来的

ThreadPoolExecutor的核心点:

在ThreadPoolExecutor中只有一个阻塞队列存放当前任务

 ForkJoinPool的核心特点:

  • 当有一个特别大的任务时,如果采用上述方式(ThreadPoolExecutor)这个大任务只会让某一个线程执行。
  • ForkJoin第一个特点时可以将一个大任务拆分成多个小任务,放到当前线程的阻塞队列种,其他的空闲线程就可以去处理有任务的线程的阻塞队列种的任务。

 举例:来一个比较大的数组,里面存满值,计算总和 :

单线程处理一个任务:

/** 非常大的数组 */
static int[] nums = new int[1_000_000_000];
// 填充值
static{
    for (int i = 0; i < nums.length; i++) {
        nums[i] = (int) ((Math.random()) * 1000);
    }
}
public static void main(String[] args) {
    // ===================单线程累加10亿数据================================
    System.out.println("单线程计算数组总和!");
    long start = System.nanoTime();
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    long end = System.nanoTime();
    System.out.println("单线程运算结果为:" + sum + ",计算时间为:" + (end  - start));
}

 多线程分而治之的方式处理

/** 非常大的数组 */
static int[] nums = new int[1_000_000_000];
// 填充值
static{
    for (int i = 0; i < nums.length; i++) {
        nums[i] = (int) ((Math.random()) * 1000);
    }
}
public static void main(String[] args) {
    // ===================单线程累加10亿数据================================
    System.out.println("单线程计算数组总和!");
    long start = System.nanoTime();
    int sum = 0;
    for (int num : nums) {
        sum += num;
    }
    long end = System.nanoTime();
    System.out.println("单线程运算结果为:" + sum + ",计算时间为:" + (end  - start));

    // ===================多线程分而治之累加10亿数据================================
    // 在使用forkJoinPool时,不推荐使用Runnable和Callable
    // 可以使用提供的另外两种任务的描述方式
    // Runnable(没有返回结果) ->   RecursiveAction
    // Callable(有返回结果)   ->   RecursiveTask
    ForkJoinPool forkJoinPool = (ForkJoinPool) Executors.newWorkStealingPool();
    System.out.println("分而治之计算数组总和!");
    long forkJoinStart = System.nanoTime();
    ForkJoinTask<Integer> task = forkJoinPool.submit(new SumRecursiveTask(0, nums.length - 1));
    Integer result = task.join();
    long forkJoinEnd = System.nanoTime();
    System.out.println("分而治之运算结果为:" + result + ",计算时间为:" + (forkJoinEnd  - forkJoinStart));
}

private static class SumRecursiveTask extends RecursiveTask<Integer>{
    /** 指定一个线程处理哪个位置的数据 */
    private int start,end;
    private final int MAX_STRIDE = 100_000_000;
    //  200_000_000: 147964900
    //  100_000_000: 145942100

    public SumRecursiveTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        // 在这个方法中,需要设置好任务拆分的逻辑以及聚合的逻辑
        int sum = 0;
        int stride = end - start;
        if(stride <= MAX_STRIDE){
            // 可以处理任务
            for (int i = start; i <= end; i++) {
                sum += nums[i];
            }
        }else{
            // 将任务拆分,分而治之。
            int middle = (start + end) / 2;
            // 声明为2个任务
            SumRecursiveTask left = new SumRecursiveTask(start, middle);
            SumRecursiveTask right = new SumRecursiveTask(middle + 1, end);
            // 分别执行两个任务
            left.fork();
            right.fork();
            // 等待结果,并且获取sum
            sum = left.join() + right.join();
        }
        return sum;
    }
}
  • 最终可以发现,这种累加的操作中,采用分而治之的方式效率提升了2倍多。
  • 但是也不是所有任务都能拆分提升效率,首先任务得大,耗时要长。

三、(重点)ThreadPoolExecutor

3.1 为什么要自定义线程池

1、首先ThreadPoolExecutor中,一共提供了7个参数,每个参数都是非常核心的属性,在线程池去执行任务时,每个参数都起决定性的作用

2、但是如果直接采用JDK提供的方式去构建,可以设置的核心参数最多就两个,这样就会导致对线程池的控制粒度很粗。所以在阿里规范中也推荐自己去自定义线程池。手动的去new ThreadPoolExecutor设置他的一些核心属性。

3、自定义构建线程池,可以细粒度的控制线程池,去管理内存的属性,并且针对一些参数的设置可能更好的在后期排查问题。

3.2 ThreadPoolExecutor中的7个参数

public ThreadPoolExecutor(
    int corePoolSize,           // 核心工作线程(当前任务执行结束后,不会被销毁)
    int maximumPoolSize,        // 最大工作线程(代表当前线程池中,一共可以有多少个工作线程)
    long keepAliveTime,         // 非核心工作线程在阻塞队列位置等待的时间
    TimeUnit unit,              // 非核心工作线程在阻塞队列位置等待时间的单位
    BlockingQueue<Runnable> workQueue,   // 任务在没有核心工作线程处理时,任务先扔到阻塞队列中
    ThreadFactory threadFactory,         // 构建线程的线程工作,可以设置thread的一些信息
    RejectedExecutionHandler handler) {  // 当线程池无法处理投递过来的任务时,执行当前的拒绝策略
    // 初始化线程池的操作
}

当核心线程满了,任务会扔到阻塞队列,当阻塞队列满了,这是再来任务,才会尝试创建非核心线程 

如果当前正在运行的核心线程数小于corePoolSize,线程池会优先创建新的核心线程来执行任务,而不是直接将任务放入队列或创建非核心线程。

3.3 ThreadPoolExecutor应用

3.3.1 JDK提供的四种拒绝策略(RejectedExecutionHandler

3.3.1.1 AbortPolicy(默认)
  • 当前拒绝策略会在无法处理任务时,直接抛出一个异常RejectedExecutionException
  • 使用场景:需要严格保证任务不丢失的场景,开发者需要显示捕获异常并处理

3.3.1.2 CallerRunsPolicy
  • 行为:由提交任务也的线程(调用者线程)直接执行被拒绝的任务
  • 使用场景:希望降低任务提交速度,同时保证任务不丢失
    • eg. 任务提交高峰期间,调用者线程直接执行任务,减轻线程池压力。
  • 注意:若提交线程(调用者线程)是main线程,可能阻塞main线程;适用于异步性要求不高的场景
3.3.1.3 DiscardPolicy
  • 行为:静默丢弃被拒绝的任务,不抛出异常,也不执行任何操作。
  • 使用场景:允许任务丢失的非关键场景。eg. 日志记录、监控数据...。
  • 注意事项:对业务任务丢失有一定的容忍度,否则会导致数据不一致。
3.3.1.4  DIscardOldestPolicy
  • 行为:丢弃队列中最旧(队列排第一位)的任务,然后尝试重新提交当前任务。
  • 使用场景:队列中的旧任务优先级低,允许用新任务替代。
  • 注意事项:若队列为优先级队列(如PriorityBlockingQueue),可能丢弃高优先级任务,需谨慎使用。
    • 队列类型数据结构容量锁机制特点
      PriorityBlockingQueue堆(数组实现)无界单锁元素按优先级排序,支持自定义比较器。
 3.3.1.5 如何设置(使用)拒绝策略

在创建线程池时,通过构造函数指定拒绝策略。

 3.3.1.6 选择拒绝策略的一些建议
  • 严格任务保障:使用AbortPolicy并结合异常处理逻辑。

  • 流量削峰:使用CallerRunsPolicy,利用调用者线程分担压力。

  • 容忍任务丢失:选择DiscardPolicyDiscardOldestPolicy,但需评估业务影响。

  • 自定义策略:实现RejectedExecutionHandler接口,根据业务需求定制逻辑(如记录日志、重试等)。

  • 当线程池已关闭(shutdown)时,任何提交的任务都会触发拒绝策略。

 3.4 (重点)ThreadPoolExecutor 源码分析

由于篇幅过长点击链接跳转 -->ThreadPoolExecutor 源码分析

四、线程池的核心参数设计规则

线程池的使用难度不大,难度在于线程池的参数并不好配置。

主要难点在于任务类型无法控制,比如任务有CPU密集型,还有IO密集型,甚至还有混合型的。

因为IO咱们无法直接控制,所以很多时间按照一些书上提供的一些方法,是无法解决问题的。

《Java并发编程实践》

想调试出一个符合当前任务情况的核心参数,最好的方式就是测试。

需要将项目部署到测试环境或者是沙箱环境中,结果各种压测得到一个相对符合的参数。

如果每次修改项目都需要重新部署,成本太高了。

此时咱们可以实现一个动态监控以及修改线程池的方案。

因为线程池的核心参数无非就是:

  • corePoolSize:核心线程数

  • maximumPoolSize:最大线程数

  • workQueue:工作队列

线程池中提供了获取核心信息的get方法,同时也提供了动态修改核心属性的set方法。

image.png

也可以采用一些开源项目提供的方式去做监控和修改

比如hippo4j就可以对线程池进行监控,而且可以和SpringBoot整合。

Github地址:GitHub - opengoofy/hippo4j: 📌 异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。Asynchronous thread pool framework, support Thread Pool Dynamic Change & monitoring & Alarm, no need to modify the code easily introduced.

官方文档:https://hippo4j.cn/docs/user_docs/intro

五、线程池处理任务的核心流程

基于addWorker添加工作线程的流程切入到整体处理任务的位置

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值