好看请点赞,养成习惯。你一个念头,我一个念头,交换之后,一个人就会有两个念头。如果你不能简单的解释它,你还不够了解它上一篇面试问我,创建多少个线程合适?怎么说呢从定性分析到定量分析,如何创建正确数量的线程,最大限度地利用系统资源(其实是几道小学数学题)。一般来说,有了这个知识点,根据需要手动创建相应数量的线程就可以了。但在现实中,你可能听说过或被问过:尽量避免手动创建线程,使用线程池统一管理线程。为什么?会有这样的要求吗?其背后的原理是什么?按照这个经验理论,肯定是手动创建线程有缺点。手动创建线程有什么缺点?Uncontrolledrisk频繁创建和不可控风险的缺点,相信你也可以说一两个系统资源有限,每个人都可以针对不同的业务手动创建线程,创建标准不同(比如线程没有名字).系统运行时,所有线程都在疯狂抢夺资源,杂乱无章,混乱的场面可想而知(如果有问题,自然不可能轻易找到并解决)如果有一个神奇的小伙伴,为每个请求创建一个线程。当大量的请求来的时候,就像一个普通的木马程序一样,无情的抽干内存,耗尽内存(你狠,你冷,你无缘无故的闹)。另外线程太多自然会造成contextSwitchingoverhead总的来说,高开销是不可控的频繁创建的高风险面试题:频繁手动创建线程有什么问题?答案:成本高这似乎是不假思索就能回答的正确答案。那我就继续问面试官:创建一个线程要花多少钱?它与我们创建普通Java对象的方式有何不同?答:……嗯……啊,按常规理解,newThread()创建线程和newObject()没有区别。Java中的一切都与对象相联系,因为Thread的祖先也是Object。如果你真的这么理解,说明你对线程的生命周期还不是很了解。请回顾一下之前Java线程的生命周期。这种理解在这篇文章中是相当简单的。在中,我们明确指出newThread()不会在操作系统级别创建新线程,这是编程语言特有的。真正转换为在操作系统层面创建线程,还要调用操作系统内核的API,然后操作系统必须为线程分配一系列资源。废话不多说,我们来对比一下两者:newObject()processObjectobj=newObject();当我需要[object]的时候,我会给自己一个新的(不知道你是不是和我一样),这个过程你应该很熟悉:分配一块内存M,在内存M上初始化对象将M的地址赋给引用变量obj就这么简单。创建线程的过程上面已经讲过了。创建线程也会调用操作系统内核API。为了更好地理解创建和启动线程的开销,我们需要看看JVM在幕后为我们做了什么:它为线程栈分配内存,线程栈为每个线程方法调用保存一个栈帧。局部变量数组、返回值、操作数栈和常量池。一些支持native方法的jvm也会分配一个nativestack。每个线程都有一个程序计数器,告诉它当前处理器正在执行什么指令。创建一个Java线程对应的native线程在JVM内部数据结构中添加线程相关的描述符线程共享堆和方法区这个描述有点抽象,用数据来说明需要创建一个线程(即使什么都不做)空间有多大?答案是1M左右java-XX:+UnlockDiagnosticVMOptions-XX:NativeMemoryTracking=summary-XX:+PrintNMTStatistics-version上图是我Java8的测试结果,19个线程,reserved和submitted都在19000+KB左右,平均大小每个线程大约1M(Java11的结果完全不一样,大家自己测试)。到这里相信你已经明白了。现在性能要求严格,频繁的手动创建/销毁线程的代价是非常巨大的,解决方案自然是你知道的线程池。什么是线程池?你常见的数据库连接池,实例池,XX池,OO池,各种池都是池化思想。总之,为了利益最大化和风险最小化,资源统一管理的思想Java也提供了自己的线程池模型的实现——ThreadPoolExecutor。套用上面的池化想象,Java线程池就是最大化高并发带来的性能提升,同时最小化手动创建线程的风险,将多个线程集中管理。为了理解这种管理思想,我们目前只需要关注ThreadPoolExecutor的构造方法publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueworkQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler){if(corePoolSize<0||maximum|Pool|maxim<=0|<0)thrownewIllegalArgumentException();if(workQueue==null||threadFactory==null||handler==null)thrownewNullPointerException();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则即使执行本地任务也不会销毁可同时执行的最大线程数。春运客流大。临时添加车辆。添加车辆后,列车总数不能超过这个最大值,否则会出现调度失败等问题(结合workqueue)3keepAliveTime表示线程池中的线程处于空闲状态当空闲时间达到这个值时,线程将被销毁,只留下corePoolSize线程位置。春运压力过后,临时加发的列车(如果空闲时间超过keepAliveTime)将被移除,只保留每日固定列车。火车班次用于日常运营。4个单位keepAliveTime的时间单位最终会换算成[纳秒],因为CPU的执行速度被keepAliveTime这个单位砍掉了,春节的计算单位是[天]5workQueue时请求的线程数大于corePoolSize的线程进入阻塞队列时,spring行程压力极大,(uptocorePoolSize)不能满足要求。所有乘车请求都会进入阻塞队列。如果排队满了,还有额外的要求,就需要加车。工厂用于生产一组具有相同任务的线程。同时,也可以通过它添加前缀名。虚拟机栈分析更清晰。比如(北京-上海)就属于火车的所有前缀。表示列车运输责任7处理者执行拒绝策略。当workQueue达到上限,也达到maximumPoolSize时,必须通过这个进行处理,比如rejection,discarding等,这是一种限流保护措施。当workQueue队列也达到线上最大队列时,maximumPoolSize会提示无票等拒绝策略,因为我们不能再添加车厢,而当前所有列车都已经满载。这是整体视图:想象一下,如果有一个请求,将创建一个新的火车,当请求结束时,火车将被“销毁”,像这样频繁的往复运动是绝对不能接受的。可见,使用线程池不仅可以完成手动创建线程可以完成的工作,还可以填补手动线程无法完成的空白。总结一下,线程池的作用包括:使用线程池管理和取线程,控制最大并发数(手动创建线程很难保证)实现任务线程队列缓存策略和拒绝机制实现一些实用功能,如定时执行、周期执行等(如火车在指定时间运行)隔离线程环境,如事务服务和搜索服务在同一台服务器上,同时开启两个线程池分别是事务线程的资源消耗明显更大。因此,通过配置独立的线程池,将较慢的事务服务和搜索服务分开,避免两个服务线程相互影响。相信你已经了解了线程池的基本思想。在使用过程中还有几点需要注意。需要说明的项目线程池使用思路/注意事项不容忽视线程池拒绝策略我们很难准确预测未来的最大并发量,所以定制合理的拒绝策略是必不可少的一步。ThreadPoolExecutor默认提供四种拒绝策略:AbortPolicy:默认的拒绝策略会抛出RejectedExecutionExceptionRejectCallerRunsPolicy:提交任务的线程自己执行任务DiscardOldestPolicy:丢弃最老的任务,其实就是进入工作队列的任务最早丢弃,然后将新任务加入工作队列DiscardPolicy:相当大胆的策略,直接丢弃任务,不抛出异常不同的框架(Netty、Dubbo)有不同的拒绝策略,我们也可以通过实现RejectedExecutionHandler来自定义策略拒绝取决于要执行的任务的重要性。如果是一些不重要的任务,可以选择直接丢弃;如果是重要任务,可以使用降级(所谓降级是服务不能正常提供功能时采取的补救措施。采用什么样的降级方式取决于具体场景)处理,比如将任务信息插入数据库或者消息队列,启用专门用于补偿补偿的线程池。没有绝对的拒绝策略,只有适合的,但是在设计过程中,不要忽视拒绝策略来禁止使用Executors创建线程池。这个问题相信很多人都看到过(阿里巴巴的Java开发手册上说禁止使用Executors创建线程池),我把源码截图(P247)在这里:Executors大大简化了我们创建各种类型线程池的方式线程池,为什么不允许使用它们?其实只要打开看看它的静态方法参数就明白publicstaticExecutorServicenewFixedThreadPool(intnThreads){returnnewThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue());}传入的workQueue是Integer.MAX_VALUE队列的一个边界,我们也可以变相的叫它无界队列,因为边界太大,这么大的等待队列也很耗内存/***Createsa{@codeLinkedBlockingQueue}withacapacityof*{@linkInteger#MAX_VALUE}.*/publicLinkedBlockingQueue(){this(Integer.MAX_VALUE);}另外,ThreadPoolExecutor方法使用了默认的拒绝策略(直接拒绝),但并不是所有的业务场景都适合使用该策略。当重要的请求过来,直接选择Rejection明显不合适er);}一般来说,使用Executors创建的线程池过于理想化,不能满足很多现实的业务场景,所以需要我们通过ThreadPoolExecutor来创建,并传入合适的参数,以便在我们需要频繁创建一个线程池时进行汇总thread,我们需要考虑到通过线程池统一管理线程资源,避免不可控的风险和额外的开销。了解了线程池的几个核心参数的概念后,我们还需要通过调优过程来设置bestthreadparameters虽然线程池的值(这个过程必不可少)弥补了手动创建线程的缺陷和差距,同时合理的降级策略可以大大增加系统的稳定性,你也应该遵循相应的说明,结合自己的实际业务场景,设置合适的参数创建线程池