zoukankan      html  css  js  c++  java
  • Java线程池总结

    一、线程池

      线程池适合处理的任务:执行时间短、工作内容较为单一。

      合理使用线程池带来的好处

        1)降低资源消耗:重复利用已创建的线程降低线程创建和销毁造成的开销

        2)提高响应速度:当任务到达时,任务可以不用等待线程创建就能立即执行

        3)提高线程的可管理性:可以统一对线程进行分配、调优和监控

        4)提供更多强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池SchedulerThreadPoolExecutor,允许任务延期执行或定期执行

      线程池使应用能更加充分利用CPU、内存、网络、IO等系统资源。线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间。

      在线程销毁时需要回收这些系统资源。因此频繁的创建和销毁线程会浪费大量的系统资源,增加并发编程风险。另外,在服务器负载过大的时候,如何让新的线程等待或者友好地拒绝服务?这些都是线程本身无法解决的。所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。线程池的作用包括:

      1):利用线程池管理并复用线程、控制最大并发数等

      2):实现任务线程队列缓存策略和拒绝机制

      3):实现某些与时间相关的功能,如定时执行、周期执行

      4):隔离线程环境。通过配置两个或多个线程池,将一台服务器上较慢的服务和其他服务隔离开,避免各服务线程相互影响。

    ThreadPoolExecutor

      UML类图:

      

      ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。

      ExecutorService接口增加了一些能力:

        (1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;

        (2)提供了管控线程池的方法,比如停止线程池的运行。

      AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。

      最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

    主要参数:

      1)corePoolSize  线程池核心线程的大小

      2)maximumPoolSize  线程池最大线程的大小

      3)keepAliveTime  空闲线程的存活时间

      4)BlockingQueue  用来暂时保存任务的工作队列

      5)RejectedExecutionHandler  线程池已经关闭或者饱和(达到了最大线程数且工作队列已满),executor()方法将调用Handler

    参数详细说明:
      1、corePoolSize  表示常驻核心线程数,如果大于0,则即使执行完任务,线程也不会被销毁(allowCoreThreadTimeOut为false)。因此这个值的设置非常关键,设置过小会导致线程频繁地创建和销毁,设置过大会造成浪费资源

      2、maximumPoolSize  表示线程池能够容纳的最大线程数。必须大于或者等于1。

      3、keepAliveTime  表示线程池中的线程空闲时间,当空闲时间达到keepAliveTime值时,线程会被销毁,避免浪费内存和句柄资源。在默认情况下,当线程池中的线程数大于corePoolSize时,keepAliveTime才起作用,达到空闲时间的线程会被销毁,直到只剩下corePoolSize个线程为止。但是当ThreadPoolExecutor的allowCoreThreadTimeOut设置为true时(默认false),核心线程超时后也会被回收。(一般设置60s)

      4、TimeUnit  表示时间单位,keepAliveTime的时间单位通常是TimeUnit.SECONDS
      5、BlockingQueue  表示缓存队列。
      6、threadFactory  表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给threadFactory增加组名前缀来实现的。在用jstack分析时,就可以知道线程任务是由哪个线程工厂产生的。
      7、handler   表示执行拒绝策略的对象。当超过workQueue的缓存上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护。友好的拒绝策略可以是如下
        三种:
        1):保存到数据库进行削峰填谷。在空闲时再取出来执行
        2):转向某个提示页面
        3):打印日志

    线程池处理任务规则及流程说明

      首先,所有的任务都是由execute方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来的执行流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。

    其执行过程如下:

      1、首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

      2、如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

      3、如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

      4、如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

      5、如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

      6、线程池中存活的线程执行完当前任务后,会在循环中反复从BlockingQueue<Runnable>队列中获取任务来执行

      

    总结使用线程池需要注意以下几点

      1、合理设置各类参数,应根据实际业务场景来设置合理的工作线程数

      2、线程资源必须通过线程池提供,不允许在应用中自行显式创建线程

      3、创建线程或线程池请指定有意义的线程名称,方便出错时回溯

      4、线程池不允许使用Executors,而是通过ThreadPoolExecutor的方式来创建,这样的处理方式能更加明确线程池的运行规则,规避资源耗尽的风险。

      另外,线程池中的线程数量不是越多越好,具体的数量需要评估每个任务的处理时间,以及当前计算机的处理器能力和数量。使用的线程过少,无法发挥处理器的性能;使用的线程过多,将会增加上下文切换的开销,反而起到相反的作用。

    合理配置线程池

      要想合理地配置线程池,就必须首先分析任务特征,可以从下面几个角度:

      1)任务的性质:CPU密集型任务,IO密集型任务或混合型任务

      2)任务的优先级:高、中、低

      3)任务的执行时间:长、中、短

      4)任务的依赖性:是否依赖其他系统资源,如数据库连接,接口调用等

      策略:

      性质不同的任务可以用不同规模的线程池分开处理。若CPU的核心数为N

      1)CPU密集型任务应配置尽可能少的线程,如配置核心数为 N+1 个线程的线程池。

      2)由于IO密集型的任务并不是一直都在执行任务,存在读写阻塞等待,因此应配置较多的线程,如核心数 2*N 个,最大线程数 25*N 

      3)混合型的任务,如果可以拆分,将其拆分为一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行的时间相差太大,则没必要进行分解。

      4)优先级不同的任务可以使用优先级队列 PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。

      5)依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。

      6)建议使用有界队列,并可以根据需要设置大一点。基于链表的LinkedBlockingQueue比基于数组的ArrayBlockingQueue吞吐量高,因为增删多,需要内存大。

     有界队列能增加系统的稳定性和预警能力,可以根据需要设置大一点,比如几千。有时会遇到任务执行缓慢的时候,为了避免任务被拒绝,可以适当将队列设置大一些。

      7)大量的耗时任务执行的时候,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先。这个时候应该设置较大队列去缓冲并发任务,调整合适的核心线程数,避免过多引发 线程上下文切换频繁导致的资源消耗(会降低处理任务的速度,降低吞吐量)

      

      参数动态化

      ThreadPoolExecutor 提供了API供ThreadPoolExecutor的实例来动态设置线程池的参数。

      

      以setCorePoolSize为例,在运行期线程池使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程数的情况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idle的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会创建新的worker线程来执行队列任务。

      结合ZooKeeper分布式配置中心,利用Zookeeper watch机制,注册节点监听,节点变动自动拉取节点配置判断线程池参数是否变化,若变化,则修改线程池参数,实现即时修改,即时生效。

      

    线程池的监控

      如果系统中大量使用线程池,则有必要对线程池进行监控,方便出现问题时,根据线程池的使用状况快速定位问题。可以通过线程池提供的方法获取线程池的信息:

      getPoolSize():获取当前线程池中线程的数量

      getActiveCount():获取大致的活动的线程数(正在执行任务的线程)

      getLargestPoolSize():获取largestPoolSize属性,线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。

      getTaskCount():返回已经计划执行的任务的大致总和

      getCompletedTaskCount():返回已完成执行的任务的大致总数,小于或等于 getTaskCount()

      也可通过直接调用 toString()方法获取大部分信息,当前线程池中线程的数量,活动的即正在执行任务的线程数量,队列大小,已经执行完的任务等。输出结果:

    java.util.concurrent.ThreadPoolExecutor@379aea30[Running, pool size = 50, active threads = 50, queued tasks = 300, completed tasks = 1987247]

                                                                                                                                                                                                                         

     创建线程池例子,拒绝策略为打印日志并由JVM重启一个线程执行被拒绝的任务:

       /**
         * 创建一个用于发送邮件的线程池,核心线程数为1,最大线程数为5,线程空闲时间为60s,拒绝策略为打印日志并直接执行被拒绝的任务
         */
        private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(50), new CustomThreadFactory("redeemSendMail"), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                // 打印日志
                LOGGER.error("Task:{},rejected from:{}", r.toString(), executor.toString());
           // 直接执行被拒绝的任务,JVM另起线程执行
           r.run();
            }
        });

    应用 

    不同场景下,线程池的参数设置策略不同。

    场景1:快速响应请求

      追求的是响应速度,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。

    场景2:快速处理批量任务

      任务量大,不需要瞬时完成,尽可能快执行完成即可。因此关注点在如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。

      所以应设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。

    END

  • 相关阅读:
    unix网络编程 初步了解TCP/IP协议
    unix网络编程 常见概念
    linux 环境变量
    linux c编程
    第二周学习笔记
    jmeter第一周学习笔记
    建造者模式
    原型设计模式
    抽象工厂模式
    工厂方法模式
  • 原文地址:https://www.cnblogs.com/yangyongjie/p/12422946.html
Copyright © 2011-2022 走看看