线程池

作者: 因你而在_caiyq | 来源:发表于2021-03-24 17:42 被阅读0次

    原创文章,转载请注明原文章地址,谢谢!

    线程池最主要的工作在于控制运行线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。从而做到线程复用、控制最大并发数量、管理线程。

    线程池优势
    • 降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能执行。
    • 提高线程的可管理性:线程是稀缺资源,不能无限创建,否则会消耗系统资源、降低系统的稳定性,使用线程可以进行统一分配,调优和监控。
    线程池中的继承实现关系
    线程池的使用方式
    • Executors.newFixedThreadPool(int):创建一个固定线程数量的线程池,可控制线程最大并发数,超出的线程需要在队列中等待。注意它内部corePoolSize和maximumPoolSize的值是相等的,并且使用的LinkedBlockingQueue。
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    }
    
    • Executors.newSingleThreadExecutor():创建一个单线程的线程池,它只有唯一的线程来执行任务,保证所有任务按照指定顺序执行。注意它内部corePoolSize和maximumPoolSize的值都为1,使用的是LinkedBlockingQueue。
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>()));
    }
    
    • Executors.newCachedThreadPool():创建一个可缓存的线程池,如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收线程,则创建新线程。注意它内部将corePoolSize值设为0,maximumPoolSize值设置为Integer.MAX_VALUE,并且使用的是SynchronousQueue,keepAliveTime值为60,即当线程空闲时间超过60秒,就销毁线程。
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
    }
    

    以上三种创建线程的方式内部都是由ThreadPoolExecutor这个类完成的,该类的构造方法有5个参数,称为线程池的5大参数。线程池使用完毕之后需要关闭,应该配合try-finally代码块,将线程池关闭的代码放在finally代码块中。

    • Executors.newScheduledThreadPool()
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    • Executors.newWorkStealingPool(),Java8提供。
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
                (Runtime.getRuntime().availableProcessors(),
                        ForkJoinPool.defaultForkJoinWorkerThreadFactory,
                        null, true);
    }
    
    线程池的参数
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
    }
    

    corePoolSize:线程池中常驻核心线程池。
    maximumPoolSize:线程池中能够容纳同时执行最大线程数,该值必须大于等于1。
    keepAliveTime:多余线程的最大存活时间。
    unit:keepAliveTime的单位。
    workQueue:任务队列,被提交但尚未被执行的任务。
    threadFactory:生成线程池中工作线程的线程工厂,一般使用默认即可。
    handler:拒绝策略,表示当任务队列满并且工作线程大于等于线程池的最大线程数时,对即将到来的线程的拒绝策略。

    线程池的底层原理

    1、在创建线程池后,等待提交过来的任务请求。
    2、当调用execute()/submit()方法添加一个请求任务时,线程池会做出以下判断

    • 如果正在运行的线程数量小于corePoolSize,会立刻创建线程运行该任务。
    • 如果正在运行的线程数量大于等于corePoolSize,会将该任务放入阻塞队列中。
    • 如果队列已满但是正在运行的线程数量小于maximumPoolSize,线程池会进行拓展,将线程池中的线程数拓展到最大线程数。
    • 如果队列满并且运行的线程数量大于等于maximumPoolSize,那么线程池会启动相应的拒绝策略来拒绝相应的任务请求。

    3、当一个线程完成任务时,它会从队列中取下一个任务来执行。
    4、当一个线程空闲时间超过给定的keepAliveTime时,线程会做出判断:如果当前运行线程大于corePoolSize,那么该线程将会被停止。即当线程池的所有任务都完成之后,它会收缩到corePoolSize的大小。


    线程池的拒绝策略

    当线程池的阻塞队列满了同时线程池中线程数量达到了maximumPoolSize时,线程池将会启动相应的拒绝策略来拒绝请求任务。

    • AbortPolicy:默认,直接抛出RejectedExecutionException异常阻止系统正常运行。
    • CallerRunsPolicy:调用者运行的一种机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者。
    • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务。
    • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果任务允许丢失,那么该策略是最好的方案。

    注意

    • 以上4种拒绝策略均实现了RejectedExecutionHandler接口。
    • 实际开发中不允许使用内置的线程池,必须明确地通过ThreadPoolExecutor方式,指定相应的线程池参数创建自定义线程或者使用其它框架提供的线程池。因为内置线程池的第五个参数阻塞队列允许的请求队列长度为Integer.MAX_VALUE,可能造成大量请求堆积,导致OOM。


    public static void main(String[] args) {
        ExecutorService executor = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());
        try {
            for (int i = 0; i < 10; i++) {
                executor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executor.shutdown();
        }
    }
    
    线程池配置合理线程数量

    线程池合理配置线程数量需要考虑业务具体是CPU密集型还是IO密集型。
    1、CPU密集型:该任务需要大量运算,而没有阻塞,CPU一直在全速运行,CPU密集型只有在真正的多核CPU上才能进行加速。

    • CPU密集型任务配置应该尽可能少的线程数量,一般公式为:CPU核数 + 1个线程的线程池

    2、IO密集型:任务需要大量的IO操作,即大量的阻塞。在单线程上进行IO密集型的任务会浪费大量的CPU运算能力在等待操作上。所以在IO密集型任务中使用多线程可以大大加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

    • IO密集型时,大部分线程都阻塞,故需要多配置线程数。CPU核数/(1-阻塞系数),阻塞系数在0.8-0.9
    • 由于IO密集型任务线程并不是一直在执行任务,则应配置尽肯能多的线程。CPU核数*2。

    博客内容仅供自已学习以及学习过程的记录,如有侵权,请联系我删除,谢谢!

    相关文章

      网友评论

        本文标题:线程池

        本文链接:https://www.haomeiwen.com/subject/pidhhltx.html