**由于不知线程池的bug,某Java程序员叕被祭天

2023-12-14 14:50

本文主要是介绍**由于不知线程池的bug,某Java程序员叕被祭天,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

说说你对线程池的理解?

首先明确,池化的意义在于缓存,创建性能开销较大的对象,比如线程池、连接池、内存池。预先在池里创建一些对象,使用时直接取,用完就归还复用,使用策略调整池中缓存对象的数量。

Java创建对象,仅是在JVM堆分块内存,但创建一个线程,却需调用os内核API,然后os要为线程分配一系列资源,成本很高,所以线程是一个重量级对象,应避免频繁创建或销毁。
既然这么麻烦,就要避免呀,所以要使用线程池!

一般池化资源,当你需要资源时,就调用申请线程方法申请资源,用完后调用释放线程方法释放资源。但JDK的线程池根本没有申请线程和释放线程的方法。

那到底该如何理解它的设计思想呢?
其实线程池的设计,采用的是生产者-消费者模式:

  • 线程池的使用方是生产者

  • 线程池本身是消费者

以下简化代码即可显示线程池的基本原理:
图片
JDK线程池最核心的就是ThreadPoolExecutor,看名字,它强调的是Executor,并非一般的池化资源。

为什么都说要手动声明线程池?

虽然JDK的Executors工具类提供的方法可快速创建线程池。
图片
但阿里有话说:
图片

弊端真的这么严重吗,newFixedThreadPool=OOM?

写段测试代码:
图片

执行不久,出现OOM

Exception in thread "http-nio-30666-ClientPoller" java.lang.OutOfMemoryError: GC overhead limit exceeded
  • newFixedThreadPool线程池的工作队列直接new了一个LinkedBlockingQueue
    图片

  • 但其默认构造器是一个Integer.MAX_VALUE长度的队列,所以很快Q满
    图片

虽然使用newFixedThreadPool可以固定工作线程数量,但任务队列几乎无界。若任务较多且执行较慢,队列就会快速积压,内存不够,易导致OOM。

newCachedThreadPool也等于OOM?

[11:30:30.487] [http-nio-30666-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread] with root cause
java.lang.OutOfMemoryError: unable to create new native thread

可见OOM是因为无法创建线程,newCachedThreadPool这种线程池的最大线程数是Integer.MAX_VALUE,也可认为无上限,而其工作队列SynchronousQueue是一个没有存储空间的阻塞队列。
图片
所以只要有请求到来,就必须找到一条工作线程处理,若当前无空闲线程就再创建一个新的。
由于我们的任务需很长时间才能执行完成,大量任务进来后会创建大量线程。而线程是需要分配一定内存空间作为线程栈的,比如1MB,因此无限创建线程必OOM

所以使用线程池,请不要抱任何侥幸,以为只是处理轻量任务,不会造成队列积压或创建大量线程!比如某业务设计用户注册后,就调用短信服务发送短信,发短信接口正常一般在100ms内响应,现在TPS=100的注册量,CachedThreadPool能稳定在占用10个左右线程情况下满足需求。
突然某时间点,短信服务不可用了!而代码里调用短信服务设置的超时又特别长, 比如1min,1min可能已经进来6000个用户的注册请求,产生6000个发短信的任务,需6000个线程,没多久就因为无法再创建新线程导致OOM。

所以阿里才不建议使用Executors:

  • 要结合实际并发情况,评估线程池核心参数,确保其工作行为符合预期,关键的也就是设置有界工作队列和数量可控的线程数

  • 永远要为自定义的线程池设置有意义名称,以便排查问题
    因为当出现线程数量暴增、死锁、CPU负载高、线程执行异常等事故时,往往都需抓取线程栈。有意义的线程名称,就很重要。

注意异常处理

使用线程池,尤其需要注意异常处理,例如通过ThreadPoolExecutor#execute()提交任务时,若任务在执行的过程中出现运行时异常,会导致执行任务的线程终止。
但致命的是任务虽然异常了,但是你却获取不到任何通知,让你有任务都执行很正常的错觉。虽然线程池提供了很多用于异常处理的方法,但最稳妥和简单的方案还是捕获所有异常并具体处理:
图片

线程池的线程管理

  • 最简陋的监控
    图片

还好有谷歌,一般我们直接利用guava的ThreadFactoryBuilder实现线程池线程的自定义命名即可。

测试线程池管理线程的策略。每次间隔1秒向线程池提交任务,循环20次,每个任务需要10秒才能执行完成。执行发现提交失败的记录,日志就像这样
图片

线程池的拒绝策略

线程池默认的拒绝策略会抛RejectedExecutionException,这是个运行时异常,IDEA不会强制捕获,所以我们也很容易忽略它。
对于采用何种策略,具体要看任务的重要性:

  • 若是一些不重要任务,可选择直接丢弃

  • 重要任务,可采用降级,比如将任务信息插入DB或MQ,启用一个专门用作补偿的线程池去补偿处理。所谓降级,也就是在服务无法正常提供功能的情况下,采取的补救措施。具体处理方式也看具体场景而不同。

  • 当线程数大于核心线程数时,线程等待keepAliveTime后还是无任务需要处理,收缩线程到核心线程数
    了解这个策略,有助于我们根据实际的容量规划需求,为线程池设置合适的初始化参数。也可通过一些手段来改变这些默认工作行为,比如:

  • 声明线程池后立即调用prestartAllCoreThreads,启动所有核心线程
    图片

  • 传true给allowCoreThreadTimeOut,让线程池在空闲时同样回收核心线程
    图片

弹性伸缩的实现

线程池是先用Q存放来不及处理的任务,满后再扩容线程池。当Q设置很大时(那个 工具类),最大线程数这个参数就没啥意义了,因为队列很难满或到满时可能已OOM,更没机会去扩容线程池了。
是否能让线程池优先开启更多线程,而把Q当成后续方案?比如我们的任务执行很慢,需要10s,若线程池可优先扩容到5个最大线程,那么这些任务最终都可以完成,而不会因为线程池扩容过晚导致慢任务来不及处理。

难题在于:

  • 线程池在工作队列满时,会扩容线程池
    重写队列的offer,人为制造该队列满的条件

  • 改变了队列机制,达到最大线程后势必要触发拒绝策略
    实现一个自定义拒绝策略,这时再把任务真正插入队列

Tomcat就实现了类似的“弹性”线程池。

务必确认清楚线程池本身是不是复用的。

某线上服务,运维监控偶尔报警线程数过多,超过2000,收到告警后查看监控,发现瞬时线程数比较多但过一会儿又会降下来,线程数抖动厉害,但应用访问量变化不大。

于是,在线程数较高时抓取线程栈,发现内存中有1000多个自定义线程池。线程池应该是复用的,有5个以内线程池都基本正常,但上千个线程池肯定不正常!

但代码里也没看到声明了线程池,于是就搜索execute关键字,定位到原来业务代码调用了一个类库:
图片
该类库竟然每次都创建一个新的线程池。
图片
newCachedThreadPool会在需要时创建必要多的线程,业务代码的一次业务操作会向线程池提交多个慢任务,这样执行一次业务操作就会开启多个线程。如果业务操作并发量较大的话,的确有可能一下子开启几千个线程。

那为何监控中看到线程数量会下降,而不OOM?

newCachedThreadPool的核心线程数是0,而keepAliveTime是60s,所以60s后所有线程都可回收。

那这如何修复呢?

使用static字段存放线程池引用。
图片

线程池的意义在于复用,就意味着程序应该始终使用一个线程池吗?

不是的。要根据任务优先级指定线程池的核心参数,包括线程数、回收策略和任务队列。

业务代码使用线程池异步处理一些内存中的数据,但监控发现处理得很慢,整个处理过程都是内存中的计算不涉及I/O操作,也需要数s处理时间,CPU占用也不高。
最终排查发现业务代码使用的线程池,还被一个后台文件批处理任务用了。
图片
这文件批处理程序,在程序启动后通过一个线程开启死循环逻辑,不断向线程池提交任务,任务的逻辑是向一个文件中写入大量的数据。可以想象,这个线程池中的2个线程任务相当重。
线程池的2个线程始终处活跃状态,队列也基本满。因为开启了CallerRunsPolicy拒绝处理策略,所以当线程满队列满,任务会在提交任务的线程或调用execute方法的线程执行,所以不能认为提交到线程池的任务就一定会被异步处理。
若使用CallerRunsPolicy,就有可能异步任务变同步执行。业务代码复用这样的线程池来做内存计算就麻烦了。

  • 向线程池提交一个简单任务
    图片

  • 简单压测TPS为85,性能差
    图片

问题没这么简单。原来执行I/O任务的线程池使用CallerRunsPolicy,所以直接使用该线程池进行异步计算,当线程池饱和的时候,计算任务会在执行Web请求的Tomcat线程执行,这时就会进一步影响到其他同步处理的线程,甚至造成整个应用程序崩溃。

如何修正?

使用独立线程池处理这种“计算型任务”。
模拟代码执行的是休眠操作,并不属于CPU绑定的操作,更类似I/O绑定的操作,若线程池线程数设置太小会限制吞吐能力:
图片

  • 使用单独的线程池改造代码后再来测试一下性能,TPS提高到1683
    图片

所以盲目复用线程池的问题在于,别人定义的线程池属性不一定适合你的任务!

最后再提一个 Java 8的parallel stream

可方便并行处理集合中的元素,共享同一ForkJoinPool,默认并行度是CPU核数-1。对于CPU绑定的任务,使用这样的配置较合适,但若集合操作涉及同步I/O操作(比如数据库操作、外部服务调用),建议自定义一个ForkJoinPool(或普通线程池)。

这篇关于**由于不知线程池的bug,某Java程序员叕被祭天的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/492953

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟 开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚 第一站:海量资源,应有尽有 走进“智听

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定