池化技术及jdk的线程池讲解

概述

程序运行的本质是消耗系统资源,线程、数据库连接等都会耗费系统的资源。线程、数据库连接等的创建、销毁等都十分消耗系统资源,所以,如果使用池化技术(线程池、数据库连接池等),可以对系统资源进行控制和使用优化。

池化技术说白了就是事先准备(也可以不准备)准备一些资源,比如事先准备好一定数量的线程放进线程池,程序要用,就到线程池里面拿,用完了不是销毁,而是归还给线程池。

池化技术的好处:

  • 降低资源消耗,因为线程用完了,不是立即销毁,而是归还给线程池,达到重复利用,使用不用频繁的创建和销毁线程,降低系统资源的消耗。
  • 提高响应速度。
  • 使得系统资源达到可控,方便管理,如果不使用线程池,而是来一个请求就创建一个线程,那么,如果并发太大,无限地创建线程,最终系统资源会被耗尽。如果使用线程池,可以设定核心线程,最大线程数等参数,如果线程都在忙,有新的请求过来,就不会在创建新线程,而是会等待或者直接拒绝请求等操作(具体看拒绝策略)。
操作

java创建线程池的三大方法:

//创建一个固定线程数的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        //创建一个只有单一线程的线程池,线程池里面只有一个线程
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        //创建一个可扩展的线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

测试FixedThreadPool:

 /**
     * 测试固定线程池
     */
    public static void testFixThreadPool(){
        //创建一个固定线程数为3的固定线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 0;i<10;i++){
                fixedThreadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " run ");
                });
            }
        }finally {
            //关闭线程池
            fixedThreadPool.shutdown();
        }


    }

在这里插入图片描述
可以看到来来回回都是这三个线程在工作。

测试SingleThreadExecutor

/**
     * 测试固定线程池
     */
    public static void testSingleThreadPool(){
        //创建一个单线程线程池
        ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
        Integer result = 0;
        try {
            for (int i = 0;i<10;i++){
                final int tempI = i;
                //如果使用execute方法,是没有返回值的,如果使用submit方法,是用返回值的
                Future<Integer> future = singleThreadPool.submit((Callable<Integer>) ()->{
                    System.out.println(Thread.currentThread().getName() + " submit ");
                    return tempI;
                });

                System.out.println(future.get());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            //关闭线程池,如果开了不关闭,程序会阻塞
            singleThreadPool.shutdown();
        }


    }

在这里插入图片描述
可以看到来来回回都只有一个线程在工作,并且上面的代码还使用了线程池的另外一种执行方法,submit,是带返回值的执行方法。不带返回值用execute方法。

测试CachedThreadPool:

/**
     * 测试固定线程池
     */
    public static void testCacheThreadPool(){
        //创建一个可伸缩线程池
        ExecutorService cacheThreadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 0;i<100;i++){
                cacheThreadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName() + " run ");
                });
            }
        }finally {
            //关闭线程池,如果开了不关闭,程序会阻塞
            cacheThreadPool.shutdown();
        }


    }

在这里插入图片描述
可以看到最大的线程是32号线程,我截取的是最大的了,它的原理是这样的,当请求来时,线程池里面有空闲线程的话,就使用该线程来处理,没有的话就创建一个新的线程来处理,因为执行100次,但是有些线程已经执行完前面的任务回到线程池了,所以线程池实际上并没有创建100个线程来处理,而是有些任务复用了一些线程。如果想要 看到100个线程效果,可以使任务睡眠2秒。
在这里插入图片描述

探索三种方法的源码并讲解线程池七大参数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
可以发现三种方法都是new ThreadPoolExecutor对象,只是参数不一样而已。
七大参数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

//corePoolSize:核心线程数
//maximumPoolSize: 最大线程数
//keepAliveTime: 空闲线程存储时间
// unit   时间单位
//BlockingQueue 阻塞队列
//ThreadFactory 线程工厂
//RejectedExecutionHandler  拒绝策略
  • corePoolSize:核心线程数,就是线程池创建时就会创建,并且生命周期跟线程池一样的线程数。这些线程在线程池被销毁前都不会被销毁。
  • maximumPoolSize:最大线程数,该线程池能创建的最大的线程数。
  • keepAliveTime:有一些非核心线程,就是maximumPoolSize减corePoolSize剩下线程,这些线程如果空闲超过这个时间,就会被销毁。
  • unit :keepAliveTime的时间单位
  • BlockingQueue :阻塞队列,当前队列的线程都在忙时,就会把请求加入到阻塞队列中,如果阻塞队列满了,但是线程数还没达到最大线程数,就创建新的线程来处理请求。如果队列满了,当前忙的线程等于最大线程数,就会使用拒绝策略对新请求就行处理,可能直接拒绝等。所以线程池可处理请求数=最大线程数+阻塞队列大小。
  • ThreadFactory :创建线程的工厂,这个一般都用默认的,Executors.defaultThreadFactory()。
  • RejectedExecutionHandler :拒绝策略,当线程全部在忙,并且阻塞队列也满了后,对新来的任务的拒绝策略。默认是抛出异常,直接拒绝。

阿里巴巴开发手册上明确表明不允许使用Executors来创建线程池,而是要通过ThreadPoolExecutor来创建线程池,使用ThreadPoolExecutor可以让我们更加明确知道线程池的运行规则,达到心中有数,避免资源耗尽。
在这里插入图片描述
可以看看newFixedThreadPool和方法的参数,虽然核心线程数和最大线程数可以固定,但是他设置的阻塞队列的大小是Integer.max,那就意味着在高并发下可能会有大量的请求堆积在阻塞队列里面,导致OOM。

而newCachedThreadPool方法的最大线程数是Integer.max,这意味着可能会有大量的线程被创建。这就达不到资源控制的效果。

public static void testThreadPoolExecutor(){
        //核心线程为3,最大线程为5,非核心线程空闲20秒后销毁,阻塞队列最大为5,拒绝策略为立即拒绝并抛出异常
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,
                5,
                20,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0;i<5;i++){
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName() + " execute ");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }

结果:
在这里插入图片描述
因为睡眠了2秒,所以第4和第5个请求会进入阻塞队列,等待已有的线程执行完再去执行队列里面的请求,因为队列没有满,所以不会创建新的线程来处理,来来回回都是那三个线程,所以第4和第5会在2秒后打印。

把循环次数设置到10。
在这里插入图片描述
先进入三个请求,由核心线程执行,然后又来了5个请求直接入队,最后来了2个请求,因为队列已满,就创建新的线程来执行,因为最大线程数为5,所以来来回回就这5个线程来执行任务。

把循环次数调到11.
在这里插入图片描述
像上面的情况进行到10个请求,当第11个请求来时,因为队列已满、并且没有空闲线程,线程数已达最大线程数,所以就直接使用设置的拒绝策略来处理线程,上面使用的是抛出异常,直接拒绝。

拒绝策略

有四种拒绝策略:

  • AbortPolicy:直接拒绝并抛出异常,像刚刚上面情况。

  • CallerRunsPolicy:把任务哪里来回哪里去,线程池是在哪个线程调用的,就回哪个线程那里。
    在这里插入图片描述
    因为是main线程调用的线程池,所以哪来回哪去。

  • DiscardPolicy:直接放弃任务,但是不抛出异常。
    在这里插入图片描述
    这里只有10个任务,第11个被自动放弃了,但是没有抛出异常。

  • DiscardOldestPolicy:舍弃最老(队首,因为队列时先进先出,所以队首的任务是最先入队、最老的任务)的任务,上面的拒绝策略都是舍弃对队尾的任务。

一些参数设置建议:

首先要看线程池执行的任务是IO密集型还是CPU密集型人任务,IO密集型通常都是一些涉及计算比较多的任务,而IO密集型通常是一些Io操作,比如操作数据卷,读写文件,操作缓存等。

如果是CPU密集型任务,可以设置核心线程数等于最大线程数等于机器的核心数,这样可以充分利用CPU,不用进行不必要的线程上下文切换。

如果是IO密集型任务,通常Cpu是比较空闲的,因为多数时间都消耗在IO上,所以可以设置线程数多一点,要结合任务的数量来设置,通常为2(具体看系统的跟业务的访问量、并发数等等)倍,这样的话,在执行IO时期内还有其他线程来处理其他请求,而不至于阻塞。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值