原文地址:https://blog.csdn.net/GFJ0814/article/details/92422245

看看这篇文章(继续学习):https://www.jianshu.com/p/362b365e1bcc

背景:线上有一个接口,3台机器总共QPS在3000左右,单机QPS在1000左右,接口响应时间2ms。为了保证接口的任何改动在上线之前能够在大流量下能够没有问题。提出想法,搭建一套流量回放环境,上线之前把代码先部署到流量回放环境。然后将线上的流量导入到流量回放环境,用真实的业务请求来做模拟测试,这个过程我们称作是流量回放。

​ 为了保证流量回放的时候,流量导入过程,不能影响正常的线上接口请求,也就是响应时间不能有变化。首先就要考虑启动一个线程来异步处理这个事情。好,按照这个想法,写了第一版本代码。(以下代码是有问题的,只怪我too young too sample)

@RequestMapping(value = "/flags", method = RequestMethod.POST)
@ResponseBody
public ServerResponse getFlagsPost(@RequestBody NewServerParam param) {
//如果流量回放开关打开,就创建线程,将请求发送到流量回放环境
if(Constant.TRAFFIC_REPLAY_FLAG){
Runnable r = ()->{
HttpClientUtil.post(trafficReplayUrl+"sample/v2/flags", JSON.toJSONString(param),10000,"application/json");
};
Thread thread = new Thread(r);
thread.start();
}
long a = System.currentTimeMillis();
ServerResponse response = new ServerResponse();
response.setFlags(serverService.getFlags(param, param.getExp_key()));
long b = System.currentTimeMillis();
LOGGER.info(logService.parseToJSon(param, response, (b - a), LOGGER, Level.INFO.getName()));
return response;
}

线下测试没有问题,一上线,大流量上来,服务器瞬间报错上万条,马上回滚。

报错提示:

​ java.lang.OutOfMemoryError: Unable to create new native thread

这个错误的意思是:程序创建的线程数量已达到上限值

剖析错误
​ JVM向操作系统申请创建新的 native thread(原生线程)时, 就有可能会碰到 java.lang.OutOfMemoryError: Unable to create new native thread 错误. 如果底层操作系统创建新的 native thread 失败, JVM就会抛出相应的OutOfMemoryError. 原生线程的数量受到具体环境的限制, 通过一些测试用例可以找出这些限制, 请参考下文的示例. 但总体来说, 导致 java.lang.OutOfMemoryError: Unable to create new native thread 错误的场景大多经历以下这些阶段:

java程序向jvm申请创建一个线程

jvm本地代码(native code)代理该请求,尝试创建一个操作系统级别的native Thread(原生线程)

操作系统尝试创建一个新的native Thread,需要同时分配一些内存给该线程

如果操作系统的虚拟内存已经耗尽,或者受到32位进程的地址空间限制(约2-4GB),OS就会拒绝本地内存分配

JVM抛出 java.lang.OutOfMemoryError: Unable to create new native thread 错误。

参考:https://blog.csdn.net/renfufei/article/details/78088553

改进方案-springBoot整合线程池优化

​ 错误很明显,就是创建线程数量过多,超过OS所能允许的最大空间。那这个问题,完全就可以用线程池去解决,用线程池维护一定数量的线程,防止无限制的创建线程,带来的内存开销过大。

代码改进:

​ 1. 创建线程池配置类

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor; /**
* @description: 线程池配置类
**/
@Configuration //表示这个类是配置类
@EnableAsync //表示这个类是线程池配置类
public class ExecutorConfig { private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);
@Bean
public Executor asyncServiceExecutor(){
logger.info("start asyncServiceExecutor");
ThreadPoolTaskExecutor executor = new VisiableThreadPoolTaskExecutor();
int corePoolSize = Runtime.getRuntime().availableProcessors();
//配置核心线程数
executor.setCorePoolSize(corePoolSize*2);
//配置最大线程数
executor.setMaxPoolSize(corePoolSize*2);
//配置队列大小
executor.setQueueCapacity(99999);
//配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("async-service-");
// rejection-policy:当pool已经达到max size的时候,并且队列已经满了,如何处理新任务
// CALLER_RUNS:不在新线程中执行任务,而是有调用者所在的线程来执行
//DiscardPolicy: 直接丢弃
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
//执行初始化
executor.initialize();
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
} }

2.创建线程信息打印的类,这样在执行线程池执行excute方法的时候,会把当前的任务的情况打印出来

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.concurrent.ListenableFuture; import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor; /**
* @description: 获取线程池的监控信息
**/
public class VisiableThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { private static final Logger logger = LoggerFactory.getLogger(VisiableThreadPoolTaskExecutor.class); private void showThreadPoolInfo(String prefix){
ThreadPoolExecutor threadPoolExecutor = getThreadPoolExecutor();
if(null == threadPoolExecutor){
return;
}
logger.info("{},{},taskCount[{}],completedTaskCount[{}],activeCount[{}],queuesize[{}]",
this.getThreadNamePrefix(),
prefix,
threadPoolExecutor.getTaskCount(),
threadPoolExecutor.getCompletedTaskCount(),
threadPoolExecutor.getActiveCount(),
threadPoolExecutor.getQueue().size()
);
} @Override
public void execute(Runnable task) {
showThreadPoolInfo("1. do execute");
super.execute(task);
} @Override
public void execute(Runnable task, long startTimeout) {
showThreadPoolInfo("2. do execute");
super.execute(task, startTimeout);
} @Override
public Future<?> submit(Runnable task) {
showThreadPoolInfo("1. do submit");
return super.submit(task);
} @Override
public <T> Future<T> submit(Callable<T> task) {
showThreadPoolInfo("2. do submit");
return super.submit(task);
} @Override
public ListenableFuture<?> submitListenable(Runnable task) {
showThreadPoolInfo("1. do submitListenable");
return super.submitListenable(task);
} @Override
public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
showThreadPoolInfo("2. do submitListenable");
return super.submitListenable(task);
} }

创建任务接口

创建任务实现类

import com.alibaba.fastjson.JSONObject;

/**
* 异步任务接口
*/
public interface AsyncService { void trfficRepalyForFlagV2(String param); } import com.alibaba.fastjson.JSONObject;
import com.lianjia.platform.sampling.util.HttpClientUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; /**
* @description: 异步任务实现类
**/
@Service
public class AsyncServiceImpl implements AsyncService {
private final Logger logger = LoggerFactory.getLogger(AsyncServiceImpl.class); @Value("${URL.trafficReplayUrl}")
private String trafficReplayUrl; //获取yml文件中配置的流量回放环境的URL @Override
@Async("asyncServiceExecutor")//这里要使用定义的线程池配置的Bean的方法名
public void trfficRepalyForFlagV2(String param) {
HttpClientUtil.post(trafficReplayUrl+"sample/v2/flags", param,10000,"application/json");
} }

使用任务

@RequestMapping(value = "/flags", method = RequestMethod.POST)
@ResponseBody
public ServerResponse getFlagsPost(@RequestBody NewServerParam param) {
//如果流量回放开关打开,就创建线程,将请求发送到流量回放环境
if(Constant.TRAFFIC_REPLAY_FLAG){
asyncService.trfficRepalyForFlagV2(JSON.toJSONString(param));
}
long a = System.currentTimeMillis();
ServerResponse response = new ServerResponse();
response.setFlags(serverService.getFlags(param, param.getExp_key()));
long b = System.currentTimeMillis();
LOGGER.info(logService.parseToJSon(param, response, (b - a), LOGGER, Level.INFO.getName()));
return response;
}

压测结果:

​ 压测数据:并发数50,压测时间10min。并发数=QPS(1000)*响应时间(0.02s)。

​ 之前因为上线之前没有做压测,导致了上线之后,大流量下报错。吃一堑,长一智。这次改完之后做了压测。压测之后,第一,打开流量开关之后,不报错了;第二,主线程平均响应耗时和不开启异步任务时候的平均响应耗时基本一致。证明方案是可以的。

插曲:拒绝策略使用不当导致主线程平均响应时间非常大。第一次在写线程池配置类的时候,使用了executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());的拒绝策略。队列的大小设置了3000,在压测的时候发现并发数50(一般计算是并发数=QPS * 响应时间)左右,这个和线上单台机器的QPS基本接近,虽然不报错了主线程的接口耗时远远超出了不打开开关的接口耗时。通过打印信息来看,是因为提交的任务量非常大,队列中的任务已经把队列填满了,这个时候,从线程池原理来看,要去创建线程数达到maxpoolsize,我们这里设置的maxPoolsize和corePoolsize大小是一样的。意味着就不会再去创建线程了,只能走拒绝策略。这里的拒绝策略CallerRunsPolicy的含义是如果异步线程执行不了,就由调用线程处理,实际上就是主线程来处理,这样就会导致主线程的部分流量回放任务成了同步的了。这当然会增大主线程的接口响应时间了。因为我们只需要有部分线上流量了其实就可以了,因此,我把拒绝策略改为了直接丢弃。这样处理不了的线程不进入队列,也不由主线程执行,保证主线程的响应时间不受影响。

关于线程池与线程的个人理解

线程池是管理线程的地方。线程池分为程序中自定义的,也有数据库自己的线程池。
需要做的就是协调俩者,避免出现程序中运行的线程过多导致等待时间过长报错。
还有就是线程池的处理策略。避免出现策略选择错误导致出错。 https://blog.csdn.net/qiuyubo1/article/details/80288525
这篇文章是介绍异步和多线程之间的区别 异步是多线程的最终期待结果。

转载-SpringBoot结合线程池解决多线程问题实录;以及自己的总结的更多相关文章

  1. SpringBoot 自定义线程池,多线程

    原文:https://www.jianshu.com/p/832f2b162450 我们都知道spring只是为我们简单的处理线程池,每次用到线程总会new 一个新的线程,效率不高,所以我们需要自定义 ...

  2. java笔记--使用线程池优化多线程编程

    使用线程池优化多线程编程 认识线程池 在Java中,所有的对象都是需要通过new操作符来创建的,如果创建大量短生命周期的对象,将会使得整个程序的性能非常的低下.这种时候就需要用到了池的技术,比如数据库 ...

  3. 基于线程池的多线程售票demo2.0(原创)

    继上回基于线程池的多线程售票demo,具体链接: http://www.cnblogs.com/xifenglou/p/8807323.html以上算是单机版的实现,特别使用了redis 实现分布式锁 ...

  4. (转载)JAVA线程池管理

    平时的开发中线程是个少不了的东西,比如tomcat里的servlet就是线程,没有线程我们如何提供多用户访问呢?不过很多刚开始接触线程的开发攻城师却在这个上面吃了不少苦头.怎么做一套简便的线程开发模式 ...

  5. Java第三阶段学习(七、线程池、多线程)

    一.线程池 1.概念: 线程池,其实就是一个容纳多个线程的容器,其中的线程可以重复使用,省去了频繁创建线程对象的过程,无需反复创建线程而消耗过多资源,是JDK1.5以后出现的. 2.使用线程池的方式- ...

  6. java线程一之创建线程、线程池以及多线程运行时间统计

    线程和进程的基本概念 进程和线程是动态的概念.         进程是 "执行中的程序",是一个动词,而程序是一个名词,进程运行中程序的"代码",而且还有自己的 ...

  7. 线程池,多线程,线程异步,同步和死锁,Lock接口

    线程池 线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源. 除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源.线程 ...

  8. SpringBoot—自定义线程池及并发定时任务模板

    介绍   在项目开发中,经常遇到定时任务,今天通过自定义多线程池总结一下SpringBoot默认实现的定时任务机制. 定时任务模板 pom依赖 <dependencies> <dep ...

  9. 基于线程池的多线程售票demo(原创)

    废话不多说,直接就开撸import org.springframework.util.StopWatch;import java.util.concurrent.*;/** * 基于线程池实现的多线程 ...

随机推荐

  1. CODING 受邀参与 DevOps 标准体系之系统和工具&技术运营标准技术专家研讨会

    2019 年 5 月 24-25 日,国内领先的一站式 DevOps 解决方案供应商 CODING 作为腾讯云的深度合作伙伴,受邀参加在成都举行的由 TC608 云计算标准和开源推进委员会主办,中国信 ...

  2. November 24th, Week 48th, Sunday, 2019

    Once you replace negative thoughts with positive ones, you will start having positive results. 淘汰消极思 ...

  3. Map随笔:有序的HashMap——LinkedHashMap

    目录 Map随笔:有序的HashMap--LinkedHashMap 一,概述 二,源码结构 三,总结 Map随笔:有序的HashMap--LinkedHashMap 一,概述 ​ LinkedHas ...

  4. Sass、LESS 和 Stylus各有千秋

    废话不多说直接上连接  为您详细比较三个 CSS 预处理器(框架):Sass.LESS 和 Stylus  

  5. laravel5+ElasticSearch+go-mysql-elasticsearch MySQL数据实时导入(mac)

    1. ElasticSearch安装 直接使用brew install elasticsearch 安装最新版本的es,基本没有障碍. 2.Laravel5 框架添加elasticsearch支持 在 ...

  6. LeetCode 49: 字母异位词分组 Group Anagrams

    LeetCode 49: 字母异位词分组 Group Anagrams 题目: 给定一个字符串数组,将字母异位词组合在一起.字母异位词指字母相同,但排列不同的字符串. Given an array o ...

  7. HTML5 3D 在智慧物业/地产管理系统中的应用

    概述 该博文主要展示采用 HT for Web 提供的可视化技术,对智慧房产.智慧物业相关方向的可视化呈现做的一点尝试. 传统的 智慧房产/楼宇自动化/智慧物业 常会采用 BIM(建筑信息模型 Bui ...

  8. R3环申请内存时页面保护与_MMVAD_FLAGS.Protection位的对应关系

    Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html 技术学习来源:火哥(QQ:471194425) R3环申请内存时页 ...

  9. 分析Jackson的安全漏洞CVE-2019-12086

    CVE-2019-12086 Description A Polymorphic Typing issue was discovered in FasterXML jackson-databind 2 ...

  10. ASP.NET Core Web 应用程序系列(一)- 使用ASP.NET Core内置的IoC容器DI进行批量依赖注入(MVC当中应用)

    在正式进入主题之前我们来看下几个概念: 一.依赖倒置 依赖倒置是编程五大原则之一,即: 1.上层模块不应该依赖于下层模块,它们共同依赖于一个抽象. 2.抽象不能依赖于具体,具体依赖于抽象. 其中上层就 ...