Java多线程(27)——JUC——线程池excutors系列(2)——ThreadPoolExcutor(1)——创建线程池的底层原理

本文详细介绍了Java中的线程池,包括`ThreadPoolExecutor`的创建方式,如`newFixedThreadPool`、`newSingleThreadExecutor`、`newCachedThreadPool`等,分析了线程池的7大参数、底层工作原理和4大拒绝策略。并讨论了实际工作中如何根据任务类型(CPU密集型或IO密集型)自定义线程池。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.概述

2.Excutors创建线程池的6种方式

2.1 Excutors.newFixedThreadPool(int)

(1)使用案例

(2)源码及特点

2.2 Excutors.newSingleThreadExecutor(int)

(1)使用案例

(2)源码及特点

2.3 Excutors.newCachedThreadPool(int)

(1)使用案例

(2)源码及特点

2.4 Excutors.newScheduledThreadPool(int)

2.5 Excutors.newSingleThreadScheduledExecutor(int)

2.6 Excutors.newWorkStealingPool(int)

3.ThreadPoolExecutor的7大参数

4.线程池的底层工作原理

5.线程池的4大拒绝策略

6.实际工作中使用哪一个线程池?——自定义线程池

6.1 自定义线程池(即自己传入参数来new ThreadPoolExcutor)使用案例

6.2 如何合理配置的线程数?

(1)CPU密集型

(2)IO密集型


1.概述

ThreadPoolExecutor 是线程池的核心实现

2.Excutors创建线程池的6种方式

2.1 Excutors.newFixedThreadPool(int)

  • 创建固定线程数的线程池

(1)使用案例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);//一个池5个线程

        //模拟10个用户来办理业务,每个用户就是来自于外部的请求线程

        try {

            for (int i = 0; i < 10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t办理业务");
                });

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

    }
}

  • 可以发现只有5个线程被复用去处理业务

(2)源码及特点

主要特点:

  • 1.创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列种等待
  • 2.创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue

2.2 Excutors.newSingleThreadExecutor(int)

  • 创建只有一个线程的线程池

(1)使用案例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();//一个池1个线程

        //模拟10个用户来办理业务,每个用户就是来自于外部的请求线程

        try {

            for (int i = 0; i < 10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t办理业务");
                });

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

    }
}

  • 可以发现只有一个线程处理任务

(2)源码及特点

主要特点:

  • 1.创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
  • 2.创建的线程池corePoolSize和maximumPoolSize值都设置为1,它使用的是LinkedBlockingQueue

注意

  • newFixedThreadPool(1,threadFactory) 不等价于 newSingleThreadExecutor
    • newSingleThreadExecutor创建的线程池保证内部只有一个线程执行任务,并且线程数不可扩展
    • 而通过newFixedThreadPool(1, threadFactory)创建的线程池可以通过setCorePoolSize方法来修改核心线程数。

2.3 Excutors.newCachedThreadPool(int)

  • 创建可扩容的带缓冲的线程池

(1)使用案例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();//一个池n个线程

        //模拟10个用户来办理业务,每个用户就是来自于外部的请求线程

        try {

            for (int i = 0; i < 10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t办理业务");
                });

                //TimeUnit.MICROSECONDS.sleep(200);

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

    }
}

当把该句不注释运行:

当把该句注释运行:

  • 可以看到这种创建方式会自动根据执行任务来分配不同数量的线程

(2)源码及特点

主要特点:

  • 1.创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
  • 2.创建的线程池的corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60s,就销毁线程

以下3种待补充

2.4 Excutors.newScheduledThreadPool(int)

2.5 Excutors.newSingleThreadScheduledExecutor(int)

2.6 Excutors.newWorkStealingPool(int)

  • java8新增,使用目前机器上可用的处理器作为它的并行级别

3.创建ThreadPoolExecutor的7大参数

通过上述前三种的源码可以发现

  • 它们都是创建ThreadPoolExecutor对象,并且它们都传入了5个参数

实际上在创建ThreadPoolExecutor对象的时候,可以向构造方法种传入7个参数,用于初始化线程池的一些参数,源码如下:

  • 1.corePoolSize:线程池中的常驻核心线程数
    • 在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务
    • 当线程池中的线程数达到corePoolSize后,就会把到达的任务放到缓存队列当中
  • 2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
    • 当一个新任务被提交到池中,如果当前运行线程小于核心线程数(corePoolSize),即使当前有空闲线程,也会新建一个线程来处理新提交的任务;如果当前运行线程数大于核心线程数(corePoolSize)并小于最大线程数(maximumPoolSize),只有当等待队列已满的情况下才会新建线程。
  • 3.keeppAliveTime:多余的空闲线程的存活时间
    • 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTIme值时,多余空闲线程会被销毁直到剩下corePoolSize个线程为止
    • 默认情况下当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize
  • 4.unit:keeppAliveTime的时间单位
  • 5.workQueue:任务队列,被提交但尚未被执行的任务
  • 6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
  • 7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何来拒绝请求执行的runnable的策略

我们使用银行场景来理解以上参数:

  • 营业厅(线程池)刚开始有两个当值窗口(核心线程)在办理业务(处理任务
  • 当办理业务的人比当值窗口少的时候,那么当值窗口就够了
  • 当办理业务的人多于当值窗口的时候,多余的乘客去候客区(阻塞队列)等着
  • 但是候客区的座位是有限的(阻塞队列是有界的),代表人已经非常多了,就开启加班窗口,开启到银行窗口最大值最大线程数
  • 当银行窗口开到了最大,候客区也坐满了,还有人来,保安就对来的人进行拒绝,告诉今天暂时办理的人太多了,其他时间再来
  • 当过了高峰期,人减少了,加班窗口长时间keepAliveTime没有被使用时(除过核心线程外新增的线程变空闲),就关闭几个加班窗口(空闲线程)

4.线程池的底层工作原理

原理图:

流程图:

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

5.线程池的4大拒绝策略

什么是拒绝策略

  • 上面已经做了解释,等待队列也已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务,这时就需要拒绝策略机制合理的处理这个问题

jdk的4种拒绝策略:

  • AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行

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

  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务

  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是一种最好的方案

  • 可以发现以上内置拒绝策略均实现了RejectedExecutionHandler接口

6.实际工作中使用哪一个线程池?——自定义线程池

阿里巴巴开发手册中描述如下:

6.1 自定义线程池(即自己传入参数来new ThreadPoolExcutor)使用案例

import java.util.concurrent.*;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        //自定义线程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
                );

        //模拟5个用户来办理业务,每个用户就是来自于外部的请求线程

        try {

            for (int i = 0; i < 5; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t办理业务");
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }

    }
}

  • 5个线程,核心线程和阻塞队列刚好能容纳(2+3),所以仅仅两个核心线程在执行任务

当把上述代码的for循环中的5增加到8

  • 可以看到启用了核心线程以外的线程直到最大线程数5来执行任务,此时线程的数量并未超过max+阻塞队列容量

当把上述代码的for循环中的8增加到9

  • 超过max+阻塞队列容量,启动拒绝策略,由于使用了AbortPolicy,抛出java.util.concurrent.RejectedExecutionException异常

以下是演示另外3种拒绝策略:将任务数增加到10

将上述代码中的拒绝策略替换为,运行

  • main线程进行的调用,所以当任务超过max+阻塞队列容量,将多余的任务会交回给它的调用者线程进行处理

将上述代码中的拒绝策略替换为,运行

  • 只有8个任务被执行,等待最久的两个被抛弃

将上述代码中的拒绝策略替换为,运行

  • 多余的任务直接被丢弃

6.2 如何合理配置的线程数?

既然在使用中都是我们自己传入该7大参数,那么如何合理配置的线程数?

我们应该判断,我们的业务是CPU密集型还是IO密集型,来进行不同的配置:

(1)CPU密集型

我们可以通过如下的代码查看我们的CPU的核数,我的是8核

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行

CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些

CPU密集型任务配置尽可能少的线程数量

一般公式:CPU核数+1个线程的线程池

(2)IO密集型

方法1:

由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2

方法2:

IO密集型,即该任务需要大量的IO,即大量的阻塞

在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待上

所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间

IO密集型时,大部分被阻塞,故需要多配置线程数

  • 参考公式:CPU核数/(1-阻塞系数)                     阻塞系数在0.8~0.9之间
  • 比如8核CPU:8/(1-0.9)=80个线程数

当然以上都是经验值,最好的方式还是根据实际情况测试得出最佳配置。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值