• 【JUC】九、线程池ThreadPool


    1、线程池

    线程池和数据库连接池的理念很相似,对于数据库连接池:普通的连接数据库是建立一个JDBC连接,执行完sql之后,就会关闭,即销毁connection对象,再次连接还需要重复上述步骤。当与数据库交互频繁时,这种模式会严重影响程序的性能,因此有了数据库连接池。对应到线程池thread pool,就是线程池里维护着多个线程,等待监督管理者分配执行任务。线程池带来的好处就是:

    • 降低资源消耗:降低避免频繁创建和销毁线程的代价
    • 提高响应速度:任务达到时,不用再等待创建线程
    • 线程管理方便:线程过多,调度开销大,用线程池可防止过分调度,且可以做统一的监控、分配、调优

    关于线程切换的例子:10 年前单核 CPU 电脑,假的多线程,像马戏团小丑玩多个球,CPU 需要来回切换。 现在是多核电脑,多个线程各自跑在独立的 CPU 上,不用频繁切换,效率高。

    2、分类

    Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executors(工具类)、ExecutorsExecutorService、ThreadPoolExecutor这几个类
    在这里插入图片描述

    线程池有以下几类:

    • 一池N线程:Executors.newFixedThreadPool(int num)
    • 一池一线程:Executors.newSingleThreadExecutor()
    • 可扩容池,根据需求创建一定数量的线程,遇强则强:Executors.newCachedThreadPool()

    3、线程池的使用

    • 创建线程池对象
    • 调用execute方法提交任务
    public class ThreadPoolDemo {
    
        public static void main(String[] args) {
            //一池五线程
            ExecutorService threadPool = Executors.newFixedThreadPool(3);
            //一池1线程
            ExecutorService threadPool1 = Executors.newSingleThreadExecutor();
            //一池可扩容线程
            ExecutorService threadPool2 = Executors.newCachedThreadPool();
            //提交10次任务到线程池
            try{
                for (int i = 1; i <= 20; i++) {
                    //提交任务到另一线程(线程池中的)
                    threadPool2.execute(() -> {
                        System.out.println(Thread.currentThread().getName() + "线程正在办理业务");
                    });
                }
            }catch(Exception e){
                e.printStackTrace();
            }finally{
                threadPool2.shutdown();
            }
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    以可扩容线程池为例:

    在这里插入图片描述

    4、工作流程

    在这里插入图片描述

    如图,此时常驻线程数为2,最大线程数为5,阻塞队列长度为3(黑点),此时来了两个任务1和2 ⇒ 直接常驻线程 ⇒ 那两个任务还执行完,又来了几个任务3、4、5 ⇒ 这时不是直接上最大线程,而是进入阻塞队列 ⇒ 此时又来了三个人6、7、8 ⇒ 发现阻塞队列也满了,那就开启最大线程处理6、7、8的业务 (注意新开的线程不是去处理阻塞队列了,阻塞队列的3、4、5还是在队列中继续等待) ⇒ 此时又来了一个任务9 ⇒ 走拒绝策略

    注意这几点:

    ExecutorService pool = Executors.newSingleThreadExecutor();
    
    • 1
    • 执行上面这句,并不会创建线程,而是执行pool.execute方法提交任务时才创建
    • 常驻线程用完了,再来任务,不是直接按最大线程数启动新线程,而是阻塞队列
    • 阻塞队列满了以后,按最大线程数启动新线程,且新线程处理的不是阻塞队列里的任务

    看下源码,从提交任务的execute方法打断点,进入execute方法:

    在这里插入图片描述

    在这里插入图片描述

    5、拒绝策略

    阻塞队列和最大线程数量都用完后,走拒绝策略,JDK内置的拒绝策略有:

    • AbortPoligy(默认):直接抛出RejectedExecutionExption异常阻止系统正常运行
    • CallerRunsPoliy:既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,谁让你来的,你找谁去
    • DiscardOldestPoliy:抛弃阻塞队列中等待最久的任务,然后把当前任务加入队列中
    • DiscardPolicy:默默地丢弃无法处理的任务,不予任何处理也不抛出异常,如果允许任务丢失,这是最好的一种策略

    6、线程池的七个参数

    查看源码可以发现,不管是三种线程池中的哪种,最后都是return new ThreadPoolExecutor,关于ThreadPoolExecutor类:

    在这里插入图片描述

    • int corePoolSize:常驻线程数量
    • int maximumPoolSize:最大线程数量
    • long keepAliveTime:线程存活时间,线程多长时间没被使用就关闭
    • TimeUnit unit:存活时间的单位
    • BlockingQueue workQueue:常驻线程用完了,再来请求线程,进入阻塞队列
    • ThreadFactory threadFactory:线程工厂
    • RejectedExecutionHandler handler:拒绝策略

    以银行为例对比:银行大厅一共有10个窗口(最大线程数量),但平时一般只开5个(常驻线程数量),某天办理业务的人很多,5个窗口不够用,其余人来了就先在大厅椅子上坐着等(阻塞队列),结果椅子坐满了,还有人陆续来,于是10个窗口全开,还来很多人,那就只能告诉新来的今天轮不到你办了(拒绝策略)。

    7、自定义线程池

    Executors工具类可以创建三种线程池,但通常自定义线程池是因为,Executors返回的线程池对象有以下两个问题:

    • 对于FixedThreadPool和SingleThreadPool,代码底层用的阻塞队列是LinkedBlockingQueue类型的,队列长度为Integer.MAX_VALUE,可能堆积大量请求,导致OOM
    • 对于CachedThreadPool,其源码中写的最大线程数量为Integer.MAX_VALUE,创建大量线程,调度难度大且会OOM
    public class ThreadPoolDemo2 {
    
        public static void main(String[] args) {
            ExecutorService threadPool = new ThreadPoolExecutor(
                    2,    //常驻或核心线程数
                    5,    //最大线程数
                    2L,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(3),  //阻塞队列
                    Executors.defaultThreadFactory(), 
                    new ThreadPoolExecutor.AbortPolicy()  //拒绝策略
            );
            try {
                for (int i = 1; i <= 20; i++) {
                    threadPool.execute(() -> {
                        System.out.println(Thread.currentThread().getName() + "线程正在办理业务");
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                threadPool.shutdown();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述

    8、什么时候考虑使用线程池?

    到这儿,线程池的作用、分类、底层代码逻辑、参数与策略的问题基本清晰,那什么时候考虑去使用线程池呢?==> 线程池适合处理耗时任务,可以充分使用目前服务器的硬件资源,加快处理速度。更确切的说是:

    • 单个任务处理时间比较短
    • 但需要处理的任务的数量大

    此时,如果不使用线程池,随意启动许多线程,容易导致系统因创建大量线程而OOM且过渡调度(过渡切换)。比如工作中遇到一个excel数据转换后批量写入库里,就可拆为一批批的小任务去insert。还有帖子说需要限制并发执行的任务数量时也可以用线程池,这儿我先想到的反而是Semaphore信号灯这个JUC辅助类。

  • 相关阅读:
    LabVIEW在应用程序和接口中使用LabVIEW类和接口
    【单片机毕业设计】【mcuclub-jk-002】基于单片机的自动门 自动感应门的设计
    微软云计算[2]之微软云关系数据库SQL Azure
    【JavaWeb从入门到实战】揭开JDBC的神秘面纱之上篇
    REST framework serializer 数据data校验失败返回状态码
    Smart-tools 免费的开发工具箱
    【周赛复盘】力扣第 86 场双周赛
    金仓数据库KingbaseES客户端编程接口指南-DCI(5. 程序示例)
    操作系统【OS】虚拟机
    【无标题】
  • 原文地址:https://blog.csdn.net/llg___/article/details/134437842