为什么阿里巴巴Java开发手册中强制要求线程池不允许使用Executors创建?

  1. 手动创建线程有什么缺点?
     1)不受控风险
            

    这个缺点,相信你也可以说出一二

    系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建标准不一样(比如线程没有名字)。当系统运行起来,所有线程都在疯狂抢占资源,无组织无纪律,混乱场面可想而知(出现问题,自然也就不可能轻易的发现和解决)

    如果有位神奇的小伙伴,为每个请求都创建一个线程,当大量请求铺面而来的时候,这好比一个正规木马程序,内存被无情榨干耗尽(你无情,你冷酷,你无理取闹)

    另外,过多的线程自然也会引起上下文切换的开销

    总的来说,不受控风险很大


     2)频繁创建开销大
         a: 创建一个线程的过程
              上面面已经提到了,创建一个线程还要调用操作系统内核API。为了更好的理解创建并启动一个线程的开销,
              我们需要看看 JVM 在背后帮我们做了哪些事情:
                     1】它为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧,
                     2】每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成,
                     3】一些支持本机方法的 jvm 也会分配一个本机堆栈,
                     4】每个线程获得一个程序计数器, 告诉它当前处理器执行的指令是什么,
                     5】系统创建一个与Java线程对应的本机线程
                     6】将与线程相关的描述符添加到JVM内部数据结构中
                     7】线程共享堆和方法区域
             这段描述稍稍有点抽象,用数据来说明创建一个线程(即便不干什么)需要多大空间呢?答案是大约  1M  左右
          
    java -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary -XX:+PrintNMTStatistics -version
    


     

    上图是我用 Java8 的测试结果,19个线程,预留和提交的大概都是19000+KB,平均每个线程大概需要 1M 左右的大小(Java11的结果完全不同,这个大家自行测试吧)

    相信到这里你已经明白了,对于性能要求严苛的现在,频繁手动创建/销毁线程的代价是非常巨大的,解决方案自然也是你知道的线程池了

  2. 什么是线程池?
     

    你常见的数据库连接池,实例池,还有XX池,OO池,各种池,都是一种池化(pooling)思想,简而言之就是为了最大化收益,并最小化风险,将资源统一在一起管理的思想

    Java 也提供了它自己实现的线程池模型—— ThreadPoolExecutor。套用上面池化的想象来说,Java线程池就是为了最大化高并发带来的性能提升,并最小化手动创建线程的风险,将多个线程统一在一起管理的思想

    为了了解这个管理思想,我们当前只需要关注 ThreadPoolExecutor 构造方法就可以了

    public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.acc = System.getSecurityManager() == null ?
                    null :
                    AccessController.getContext();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
    }

    这么复杂的构造方法在JDK中还真是不多见,为了个更形象化的让大家理解这几个核心参数,我们以多数人都经历过的春运(北京——上海)来说明
     

    序号参数名称参数解释春运形象说明
    1corePoolSize表示常驻核心线程数,如果大于0,即使本地任务执行完也不会被销毁日常固定的列车数辆(不管是不是春运,都要有固定这些车次运行)
    2maximumPoolSize表示线程池能够容纳可同时执行的最大线程数春运客流量大,临时加车,加车后,总列车次数不能超过这个最大值,否则就会出现调度不开等问题
    3keepAliveTime表示线程池中线程空闲的时间,当空闲时间达到该值时,线程会被销毁,只剩下 corePoolSize 个线程位置春运压力过后,临时的加车(如果空闲时间超过keepAliveTime)就会被撤掉,只保留日常固定的列车车次数量用于日常运营
    4unitkeepAliveTime 的时间单位,最终都会转换成【纳秒】,因为CPU的执行速度杠杠滴keepAliveTime 的单位,春运以【天】为计算单位
    5workQueue当请求的线程数大于 maximumPoolSize 时,线程进入该阻塞队列春运压力异常大,即便加车后(达到maximumPoolSize)也不能满足要求,所有乘坐请求都会进入该阻塞队列中排队
    6threadFactory顾名思义,线程工厂,用来生产一组相同任务的线程,同时也可以通过它增加前缀名,虚拟机栈分析时更清晰比如(北京——上海)就属于该段列车所有前缀,表明列车运输职责
    7handler执行拒绝策略,当 workQueue 达到上限,同时也达到 maximumPoolSize 就要通过这个来处理,比如拒绝,丢弃等,这是一种限流的保护措施当workQueue排队也达到队列最大上线,maximumPoolSize 就要提示无票等拒绝策略了,因为我们不能加车了,当前所有车次已经满负载
    整体来看就是这样:

    试想,如果有请求就新建一趟列车,请求结束就“销毁”这趟列车,频繁往复这样操作,这样的代价肯定是不能接受的。

    可以看到,使用线程池不但能完成手动创建线程可以做到的工作,同时也填补了手动线程不能做到的空白。归纳起来说,线程池的作用包括:

    1)利用线程池管理并服用线程,控制最大并发数(手动创建线程很难得到保证)
    2)实现任务线程队列缓存策略和拒绝机制
    3)实现某些与实践相关的功能,如定时执行,周期执行等(比如列车指定时间运行)
    4)隔离线程环境,比如,交易服务和搜索服务在同一台服务器上,分别开启两个线程池,交易线程的资源消耗明显要大。因此,通过配置独立的线程池,将较慢的交易服务与搜索服务个离开,避免个服务线程互相影响

  3. 线程池使用思想/注意事项
     1)不能忽略的线程池拒绝策略
                 我们很难准确的预测未来的最大并发量,所以定制合理的拒绝策略是必不可少的步骤。默认情况,
            ThreadPoolExecutor 提供了四种拒绝策略:
               
             1)AbortPolicy:默认的拒绝策略,会 throw RejectedExecutionException 拒绝
             2)CallerRunsPolicy:提交任务的线程自己去执行该任务
             3)DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列
             4)DiscardPolicy:相当大胆的策略,直接丢弃任务,没有任何异常抛出
            不同的框架(Netty,Dubbo)都有不同的拒绝策略,我们也可以通过实现 RejectedExecutionHandler 自定义的拒绝策略
            对于采用何种策略,具体要看执行的任务重要程度。如果是一些不重要任务,可以选择直接丢弃;如果是重要任务,可以采用降级(所谓降级就是在服务无法正常提供功能的情况下,采取的补救措施。具体采用何种降级手段,这也是要看具体场景)处理,例如将任务信息插入数据库或者消息队列,启用一个专门用作补偿的线程池去进行补偿
            没有绝对的拒绝策略,只有适合那一个,但在设计过程中千万不要忽略掉拒绝策略就可以
     2)禁止使用Executors创建线程池
          相信很多人都看到过这个问题(阿里巴巴Java开发手册说明禁止使用 Executors 创建线程池):
          
         

    Executors 大大的简化了我们创建各种类型线程池的方式,为什么还不让使用呢?

    其实,只要你打开看看它的静态方法参数就会明白了


    传入的workQueue 是一个边界为 Integer.MAX_VALUE 队列,我们也可以变相的称之为无界队列了,因为边界太大了,这么大的等待队列也是非常消耗内存的
    另外该 ThreadPoolExecutor方法使用的是默认拒绝策略(直接拒绝),但并不是所有业务场景都适合使用这个策略,当很重要的请求过来直接选择拒绝显然是不合适的

    总的来说,使用 Executors 创建的线程池太过于理想化,并不能满足很多现实中的业务场景,所以要求我们通过 ThreadPoolExecutor来创建,并传入合适的参数
  4. 总结
     

    当我们需要频繁的创建线程时,我们要考虑到通过线程池统一管理线程资源,避免不可控风险以及额外的开销

    了解了线程池的几个核心参数概念后,我们也需要经过调优的过程来设置最佳线程参数值(这个过程时必不可少的)

    线程池虽然弥补了手动创建线程的缺陷和空白,同时,合理的降级策略能大大增加系统的稳定性

    阿里巴巴手册都是前辈们无数填坑后总结的精华,你也应该遵守相应的指示,结合自己的实际业务场景,设定合适的参数来创建线程池

转载于:https://mp.weixin.qq.com/s/scy1LEDC9_8j263BDtJerQ

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值