• java基础之浅聊线程池


    前面一直聊了三种创建线程的方法,通过类Thread,接口Runnable,接口Callable。

    当然还有一种那就是通过线程池,对于这个名字很容易想到的是连接池,虽然是两个东西,但是其优点差不多。

    线程池的作用就是控制运行的线程数量,处理过程中任务放入队列,然后在线程创建后启动这些任务。

    如果线程数量超过线程池最大数量的时候,超初的线程就去判断等候区,等正在运行的线程执行完毕之后,在冲队列中取出任务来执行。

    其特点如下:

    • 降低资源消耗,毕竟可以重复利用已创建好点线程。所以节约了线程创建和销毁的性能消耗。

    • 提高了线程的可管理性,毕竟线程是稀有资源,无效创建线程不但不会提高程序的运行效率还会降低效率以及系统的稳定性。 使用线程池可以统一调配监控。

    • Java中的线程池是通过Executor框架实现的,该框架使用Executor,Executors,ExecutorService,ThreadPoolExcutor几个类。还是老规矩盗一张图:

    在这里插入图片描述

    • Excutor:解耦了任务和任务的执行
    • Excutors:生产具体执行器的静态工厂
    • AbstractExecutorService:为各个执行器类提供基础
    • ThreadPoolExecutor:用线程池的方式管理线程
    • ScheduleThreadPoolExecutor:提供周期性执行任务支持

    但是本篇不涉及一些底层原理,而是主要聊是如何用线程池,以及在使用线程池时候实际是如何用的。

    线程池

    在创建线程池的时候Java为了方便程序员使用,通过Executors这个工具类提供一些线程池传创建的方法:

    在这里插入图片描述

    可以看出里面有很多方法,现在只聊三个提供的创建线程池的方法:

    • newFixedThreadPool

      这个会创建一个线程池,然后规定可以同时运行的有多少线程。然后进行重复使用创建的线程。

    • newSingleThreadExecutor

      这个线程池有些不像是线程池,因为线程池中只有一个线程,通俗的说就是单线程池子。

    • newCachedThreadPool

      这个线程池,不过这个线程池是弹性的,也就是随着线程的多少会自我调整池子大小。

    然后看三个创建线程池的方法,然后返回的都是ExecutorService类,所以说需要通过ExecutorService的方法进行控制池子的:

    在这里插入图片描述
    在这里插入图片描述

    其父类还有一个方法:

    在这里插入图片描述

    现在开始演示:

    newFixedThreadPool 方法

    这个可以看出就是创建一个同时可以运行某几个线程的池子,岂有两种方法:

    newFixedThreadPool(int nThreads);//线程可以同时运行的数目 nThreads
    
    newFixedThreadPool(int nThreads,  ThreadFactory threadFactory);// 线程可以同时运行的数目 nThreads  ,threadFactory就是创建线程工厂返回线程而已
    
    
    • 1
    • 2
    • 3
    • 4

    现在开始演示:

    public class test {
        public static void main(String[] args) {
          ExecutorService  threadPool= Executors.newFixedThreadPool(3);
            for (int i = 0; i < 10; i++) {
                threadPool.execute(()->{
                    System.out.println("输入的线程是"+Thread.currentThread().getName());
                });
            }
            threadPool.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    可以看出其运行的一共有三个线程,无论把线程添加多少起运行的线程一直是3条或者说是同时运行的是3条线程。

    newSingleThreadExecutor

    看一下起创建的方式:

    在这里插入图片描述

    然后用如下代码演示:

    public class test {
        public static void main(String[] args) {
          ExecutorService  threadPool= Executors.newSingleThreadExecutor();
            for (int i = 0; i < 10; i++) {
    //            继承的是父类的方法execute
                threadPool.execute(()->{
                    System.out.println("输入的线程是"+Thread.currentThread().getName());
                });
            }
    //         需要吧线程池关闭
            threadPool.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    可以看出其运行的是一个线程。

    newCachedThreadPool

    这个线程池很有意思,会动态调整的先看方法:

    在这里插入图片描述

    然后演示:

        public static void main(String[] args) {
          ExecutorService  threadPool= Executors.newCachedThreadPool();
            // 一个是30 然后再试一下50 然后看截图
            for (int i = 0; i <30; i++) {
    //            继承的是父类的方法execute
                threadPool.execute(()->{
                    System.out.println("输入的线程是"+Thread.currentThread().getName());
                });
            }
    //         需要吧线程池关闭
            threadPool.shutdown();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    电脑内存不同,可能结果不同,本人电脑设置30个线程,最后发现线程池是14个

    在这里插入图片描述

    然后运行50个线程试一下:本人电脑运行时19个

    在这里插入图片描述

    说实话这样用线程池很方便,但是一般不会如此用,而是通过ThreadPoolExecutor。

    ThreadPoolExecutor

    上面介绍的三个创建线程池的方式,然后看一下源码会发现:

      public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    
      public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
     public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    发现没有提创建线程池都是通过new ThreadPoolExecutor这个对象而创建的。为什么用这个,咱看一下阿里的规范就明白了:

    阿里的代码规范:
    【强制】线程池不允许使用Executors 去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规 则,规避资源耗尽的风险。

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

    • FixedThreadPool和 SingleThreadPool:

      允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。也可能导致栈溢出。

    • CachedThreadPool和 ScheduledThreadPool

      允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。也可能导致栈溢出。

      Integer.MAX_VALUE : java int 类型整数的最大值是(2 的 31 次方) - 1 = 2147483648 - 1 = 2147483647(21亿多) (java中 int类型 4Byte(字节) = 32bit(位))

    所以明白了为什么要使用ThreadPoolExecutor来创建线程池了吧,现在开始聊一下ThreadPoolExecutor。

    然后翻看ThreadPoolExecutor的构造方法:

    ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    其有七个参数,现在依次聊一下七个参数是什么意思:

    参数描述
    int corePoolSize线程池里面可以共享多少个线程数量,也就是同时可以运行的线程数量,也就是常驻线程数量
    int maximumPoolSize线程池其最大的线程池数量,也就是如果现有的线程池的线程不够用,最多可以调用线程的数目
    long keepAliveTime这个是线程的存储时间,如果设置了线程可以动态调整,自然如果线程足够了,自然会有一个时间来将其释放来解放性能,不过这个数字是时间单位之前的量词,所以需要与后面的一个参数一起用,那就是时间单位。
    TimeUnit unit这个时间单位。
    BlockingQueue workQueue阻塞线程,这个前面一篇聊了这个东西传送阵毕竟是一个运行的池子,而超过池子的线程放在的缓存队列中。
    ThreadFactory threadFactory,线程工厂,也就是创建线程的工厂。
    RejectedExecutionHandler handler这个是一个拒绝策略,下面补充

    整体的要给架构图如下:

    在这里插入图片描述

    其中对于拒绝策略需聊一下:

    拒绝策略

    看一下官网:

    在这里插入图片描述

    可以看出岂有四个拒绝策略,现在依次聊一下拒绝测试的不同。

    AbortPolicy

    在这里插入图片描述

    AbortPolicy异常时默认异常:直接抛出rejectedExecution异常。

    因为这个的意思就是所有的队列以及设置了最大的线程数量也满了(最大都满了意味着核心线程池的数量肯定也满了),然后还要继续放入数据,直接返回错误。

    CallerRunsPolicy

    在这里插入图片描述

    CallerRunsPolicy拒绝策略更有有意思:其时就是调用者运行的机制。简单的说就是某线程调用了线程池发现什么都满了,然后直接退回去说你调用的,这里已经满载了你自己运行吧。如果现在在就执行将返回的线程运行完,如果在回来之前调用的线程已经消失了那就直接舍弃这个任务。有点像是邮局某种原因被退的信,如果退信地址都被拆了那就直接将信废弃。

    DiscardOldestPolicy

    在这里插入图片描述

    DiscardOldestPolicy这个策略就有点喜新厌旧了,如果信赖的线程发现线程池已满载,然后就看阻塞队列中放入最早的线程之间扔了,然后将其放在队列之中,然后尝试或者说是等着向线程池中执行。

    DiscardPolicy

    在这里插入图片描述

    DiscardPolicy策略其实更有意思其默认是抛弃无法处理的业务,对于这种业务不做任何处理也不抛出异常,直接舍弃。对于一些允许业务丢失从场景是最好的一种策略,毕竟效率高。

    自定义线程池

    现在看是搞一个自定义的线程池是如何使用的,现在开始

    不过首先说一下看起构造方法:

    在这里插入图片描述

    可以看出通过自定义线程池不一定非要写7个参数,其中前面5个必写,后2个可以选择其中1个或者2个都写。

    其中拒绝策略默认为AbortPolicy,线程工厂默认自动创建。

    不过咱们演示直接就是7个参数来演示。

    public class test {
        public static void main(String[] args) {
            ThreadPoolExecutor threadpool=  new ThreadPoolExecutor(3,
                    3,
                    10,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(5),
                    new ThreadFactory() {
                        @Override
                        public Thread newThread(Runnable r) {
                            String threadname= String.valueOf(UUID.randomUUID());
                            return  new Thread(r,threadname);
                        }
                    },
    //                因为聊的四种策略是ThreadPoolExecutor类中的内部类,所以需要这样创建
                    new ThreadPoolExecutor.AbortPolicy()
    
    
            );
         System.out.println(threadpool);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这样可以看输出的池子是什么

    在这里插入图片描述

    可以看出池子本身没有运行,所以需要提交线程进入,才可以的。

    所以如下测试:

    public class test {
        public static void main(String[] args) {
            ThreadPoolExecutor threadpool = new ThreadPoolExecutor(3,
                    3,
                    10,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(5),
                    new ThreadFactory() {
                        @Override
                        public Thread newThread(Runnable r) {
                            String threadname = String.valueOf(UUID.randomUUID());
                            return new Thread(r, threadname);
                        }
                    },
    //                因为聊的四种策略是ThreadPoolExecutor类中的内部类,所以需要这样创建
                    new ThreadPoolExecutor.AbortPolicy()
    
    
            );
            System.out.println("添加线程到线程池之前:"+threadpool);
            for (int i = 0; i < 10; i++) {
                threadpool.execute(new testThread());
            }
            System.out.println("添加线程到线程池之后:"+threadpool);
            threadpool.shutdown();
        }
    }
    
    class testThread implements Runnable {
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }
    
    • 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
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    在这里插入图片描述

    报错了,为什么?

    因为采用的是AbortPolicy策略,所以不够自然会报错,因为核心线程和最大线程为3,而阻塞队列最多放3个,也就是6个,而此时我们放入10个自然会报错。

    可以把最大线程池放大或者把阻塞队列放大,再或者就是换一个拒绝策略,虽然不会报错了,但是也会根据自己的要求而舍弃一些策略。

    比如把策略换成:

    // 放弃最早放入的阻塞队列的线程  
    new ThreadPoolExecutor.DiscardOldestPolicy()
    
    • 1
    • 2

    在这里插入图片描述

    会发现最后只有9个线程会舍弃一个线程。

    如果把最大的线程池子换成10,而且策略还是AbortPolicy的结果呢?

    在这里插入图片描述

    直接运行完毕,没有问题,还可以注意线程池变成了4个,其核心(或者说常驻)线程为3个。

    其方法有很多,不再一一演示,而是简单的把官方方法黏贴一下了:

    在这里插入图片描述

    在这里插入图片描述

    本篇也就是简单说了一下自定义线程池如何使用,算是一个了解其调用的使用。

  • 相关阅读:
    adb: error: 46-byte write failed: Invalid argument
    Python问答题(更新中)
    【无标题】
    这些基本语法规则你还不知道?那你的Python还没入门...
    Transformer的应用
    护眼灯真的有用吗?2022双十二选哪个牌子的护眼台灯好
    多层板交期怎么才能有效把控?
    【golang】Windows环境下Gin框架安装和配置
    STM32WB55的FUS更新及协议栈固件烧写方法
    数据结构与算法5-栈
  • 原文地址:https://blog.csdn.net/u011863822/article/details/126728554