-
工作队列设置的很大,甚至是无界,在高负载的情况下,任务堆积在工作队列,极易发生OOM,同时
maximumPoolSize
参数和拒绝策略也不会派上用场。 -
工作队列使用了有界阻塞队列,也能用上
maximumPoolSize
,但是maximumPoolSize
设置的很大,甚至是Integer.MAX_VALUE
,在高负载的情况下,使得工作线程数很快达到corePoolSize
,并填满工作队列,此时扩容启动大量新工作线程,非常消耗资源,拒绝策略也难以触发。 -
官方提供了四种默认的拒绝策略,有直接抛异常的,有丢弃最老任务执行新任务的,也有什么也不做的,还有直接执行任务代码的(这可是串行执行任务代码),非常影响一个服务的吞吐量。而抛异常、抛弃任务等措施对于重要任务在实际工作中又不适合,所以建议根据业务情况自定义拒绝策略,而在实际工作中自定义拒绝策略往往和降级策略配合使用。
-
任务在
Worker
线程中执行时发生异常,但是Worker
线程却没有对异常做什么处理,只是上抛,则会导致Worker
线程退出销毁,开发者也不易察觉到。 -
把线程池当做线程一样使用,不复用,创建大量的线程池,浪费资源。
JUC官方提供了Executors
工具类创建线程池,希望为使用者提供方便,但是根据大量的实践和血淋淋的教训,业界都强烈禁止使用Executors
创建线程池。理由如下:
-
newFixedThreadPool
和newSingleThreadExecutor
底层使用了无界阻塞队列LinkedBlockingQueue
作为工作队列,其队列容量是Integer.MAX_VALUE
,若任务执行比较耗时,且存在大对象,在高并发情况下,工作线程数马上达到核心线程数,则继续加入到工作队列,任务逐渐堆积,非常容易发生OOM
,而 OOM 会导致所有请求都无法处理,甚至影响同一个服务其他业务的正常运行。 -
newCachedThreadPool
使用了容量为0的SynchronousQueue
作为工作队列,maximumPoolSize
却设置为Integer.MAX_VALUE
,当工作线程达到核心线程数,再提交任务想加到工作队列时失败,转而启动新的工作线程,而这个工作线程数最大值是Integer.MAX_VALUE
,一个服务创建大量的线程是非常消耗资源的,甚至影响到服务其他业务的正常运行。
业界已经禁止使用Executors
创建线程池,建议使用原生的ThreadPoolExecutor
,但是ThreadPoolExecutor
参数很多,完全需要自己配置,虽然知道每个参数的含义,但是却不知道设置为多少合适。
线程池的优化就是降低线程池的运行延迟和提高其吞吐量。优化的思路主要有两个方向,一个是优化任务代码,另一个是让CPU和IO资源利用率最大化。
如果只有一个线程,执行 CPU 计算的时候,I/O 设备空闲;执行 I/O 操作的时候,CPU 空闲,所以 CPU 的利用率和 I/O 设备的利用率都是 50%。
如果有两个线程,当线程 A 执行 CPU 计算的时候,线程 B 执行 I/O 操作;当线程 A 执行 I/O 操作的时候,线程 B 执行 CPU 计算,这样 CPU 的利用率和 I/O 设备的利用率就都达到了 100%。
线程池参数调优,比如corePoolSize
设置多大,maximumPoolSize
设置多大,工作队列容量设置多大等。工作队列不宜设置为Integer.MAX_VALUE
,需要根据服务的内存评定,比较简单;corePoolSize
和maximumPoolSize
的设置相对复杂,涉及到提交任务的类型,是CPU密集型,还是IO密集型?不同的任务类型,计算线程数方式也不同。
1、CPU密集型
CPU密集型公式: 最佳线程数=CPU 核数 +1
对于 CPU 密集型,多线程本质上是为了提升多核 CPU 的利用率,所以对于一个 4 核的 CPU,每个核一个线程,理论上创建 4 个线程就可以了,再多创建线程也只是增加线程切换的成本。
所以,对于 CPU 密集型场景,理论上“线程的数量 =CPU 核数”就是最合适的。不过在实际项目中,线程的数量一般会设置为“CPU 核数 +1”,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。
2、IO密集型
IO密集型相对于CPU密集型需要的线程数就要多一些,为什么呢?
因为IO设备的读写速度远低于CPU的执行速度,所以IO密集型的任务执行时间要比CPU密集型长很多。而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,可以多配置一些线程,根据经验具体的计算方法是:最佳线程数=2*CPU核数
。
3、IO密集型和CPU密集型交叉运行
在实际的程序中IO密集型和CPU密集型往往是交叉运行,如果再使用上述纯IO密集型计算方式得到线程数,就不太合理了
比如IO/CPU的比率很大,比如10倍,2核,较佳配置:2*(1+10)=22个线程,而2*CPU核数+1 = 5,这两个差别就很大了。
在单核的情况下,对于 I/O 密集型的计算场景,如果 CPU 计算和 I/O 操作的耗时是 1:1,那么 2 个线程是最合适的。如果 CPU 计算和 I/O 操作的耗时是 1:2,那多少个线程合适呢?是 3 个线程。CPU 在 A、B、C 三个线程之间切换,对于线程 A,当 CPU 从 B、C 切换回来时,线程 A 正好执行完 I/O 操作。这样 CPU 和 I/O 设备的利用率都达到了 100%。
通过上述例子发现,对于 I/O 密集型和CPU密集型交叉运行的场景,最佳的线程数是与程序中 CPU 计算和 I/O 操作的耗时比相关的,可以总结出一个公式:最佳线程数 =1 +(I/O 耗时 / CPU 耗时)
。令 N=I/O 耗时 / CPU 耗时,当一个线程 执行 IO 操作时,另外 N个线程正好执行完各自的 CPU 计算,这样 CPU 的利用率就达到了 100%。
上面的公式针对的单核 CPU ,至于多核 CPU,只需要等比扩大即可,计算公式如下:
最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
在实际使用中,如果因为调用第三方的插件无意或有意创建了很多线程池,这样会消耗大量资源,线程池也没有起到复用资源的作用。所以需要复用线程池。
但是复用线程池,不是说所有的业务都用一个线程池,可以根据业务的性质使用不同的线程池,达到隔离环境影响的目的。
2021年Java中高级面试必备知识点总结
在这个部分总结了2019年到目前为止Java常见面试问题,取其面试核心编写成这份文档笔记,从中分析面试官的心理,摸清面试官的“套路”,可以说搞定90%以上的Java中高级面试没一点难度。
本节总结的内容涵盖了:消息队列、Redis缓存、分库分表、读写分离、设计高并发系统、分布式系统、高可用系统、SpringCloud微服务架构等一系列互联网主流高级技术的知识点。
目录:
(上述只是一个整体目录大纲,每个点里面都有如下所示的详细内容,从面试问题——分析面试官心理——剖析面试题——完美解答的一个过程)
部分内容:
对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。
不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。
不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注