项目技术分析-线程池+异步处理

使用背景

涉及到系统数据分析与结果读写,数据量较大,串行处理较慢,因此进行分批操作,多个任务之间互不干扰;

初识异步

一些概念

  • 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
  • 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作。而异步方法通常会在另外一个线程中,“真实”地执行着。整个过程,不会阻碍调用者的工作

为什么要使用异步

总:提升性能和容错性

  • 第一个原因:容错性、健壮性,如果数据写入出现异常,不能因为写数据出现异常而导致 处理数据出现异常; 因为用户注册是主要功能,送积分是次要功能,即使送积分异常也要提示用户注册成功,然后后面在针对积分异常做补偿处理。
  • 第二个原因:提升性能,例如注册用户花了20毫秒,送积分花费50毫秒,如果用同步的话,总耗时70毫秒,用异步的话,无需等待积分,故耗时20毫秒。

异步实现方式-基于线程池

1、@Async注解来执行异步任务,需要我们手动去开启异步功能,开启的方式就是需要添加@EnableAsync
2、ThreadPoolTaskExecutor手动实现

初识线程池-ThreadPool

线程池种类(主要四种)

1、newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
2、newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
3、newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
4、newScheduledThreadPool:适用于执行延时或者周期性任务

核心参数设置

JAVA并发编程
可以看到默认线程池的队列大小和最大线程数都是Integer的最大值,显然会给系统留下一定的风险隐患

asks :每秒的任务数,假设为500~1000
taskcost:每个任务花费时间,假设为0.1s
responsetime:系统允许容忍的最大响应时间,假设为1s

CPU密集型任务 re
尽量使用较小的线程池,最大线程数=CPU核心数+1。
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换
IO密集型任务(本项目)
可以使用稍大的线程池,最大线程数=2*CPU核心数。
**IO密集型任务CPU使用率并不高,**因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
同时 核心线程数 = 最大线程数 * 20%

线程池执行步骤

1、当池子大小小于corePoolSize,就新建线程,并处理请求
2、当池子大小等于corePoolSize,把请求放入workQueue(QueueCapacity)中,池子里的空闲线程就去workQueue中取任务并处理
3、当workQueue放不下任务时,就新建线程入池,并处理请求,如果池子大小撑到了maximumPoolSize,就用RejectedExecutionHandler来做拒绝处理
4、当池子的线程数大于corePoolSize时,多余的线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁

具体实现

应用一-注解**@Async**方式

参考文章
对于异步方法调用,从Spring3开始提供了@Async注解,我们只需要在方法上标注此注解,即可实现异步调用。
此外,我们还需要一个配置类,通过Enable模块驱动注解@EnableAsync 来开启异步功能。

@Configuration
@EnableAsync
public class ThreadPoolConfig {

}

线程池必须手动通过 ThreadPoolExecutor 的构造函数来声明,使用@Async注解,在默认情况下用的是SimpleAsyncTaskExecutor线程池,该线程池不是真正意义上的线程池

线程池配置

参考文章 作者:飘渺Jam
使用此线程池无法实现线程重用,每次调用都会新建一条线程。若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误

扫描二维码关注公众号,回复: 15407959 查看本文章
// 核心线程池大小
private int corePoolSize = ;

// 最大可创建的线程数
private int maxPoolSize = ;

// 队列最大长度
private int queueCapacity = ;

// 线程池维护线程所允许的空闲时间
private int keepAliveSeconds = ;

业务类

@Component
public class UserDataHandler {
	
 
	private static final Logger LOG = LoggerFactory.getLogger(SyncBookHandler.class);
	/**
	 * @param userdataList 一段数据集合
	 * @param pageIndex 段数
	 * @return Future<String> future对象
	 * @since JDK 1.8
	 */
	@Async
	public Future<String> syncUserDataPro(List<UserData> userdataList,int pageIndex){
		
 
			//声明future对象-主要是为了返回处理信息
		 	Future<String> result = new AsyncResult<String>("");
		 	//循环遍历该段旅客集合
			if(null != userdataList && userdataList.size() >0){
				for(UserData userdata: userdataList){
					try {
						//数据入库操作
						// 针对每一个获取到的切割子段,进行操作 同时进行
					} catch (Exception e) {
						
						//记录出现异常的时间,线程name
						result = new AsyncResult<String>("fail,time="+System.currentTimeMillis()+",thread id="+Thread.currentThread().getName()+",pageIndex="+pageIndex);
						continue;
					}
				}
			}
			return result;
		}

这里要注意以下几点:
1、批量数据-异步操作实现 需要分割数据 因此这里有循环切片
2、使用了Async注解后要么void不返回值,要么只能返回Future类型的值,否则注解失效;文中案例是为了返回执行信息,所以使用Future。
3、至于数据库异常的场景,需要看具体业务要求需不需要事务、回滚;在这里我的业务场景允许数据丢失;

使用-CountDownLatch

目的:保证之前所有线程都执行完毕再走下一步
虽然实现了异步请求接口,效率上提升了很多。但是,由于异步调用的原因,数据还没处理完就会返回处理结果。我们必须等待syncUserDataPro中的所有线程结束后,再返回当前调用线程任务的方法

List<List<UserData>> lists = Lists.partition(res, 10);
CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (List<UserData> list : lists) {
      while (iterator.hasNext()) {
            UserData userData = iterator.next();
            this.userDataService.asyncinsertUserDataList(userData, countDownLatch);
      }
}
countDownLatch.await(); //保证之前的所有的线程都执行完成,才会走下面的;
log.info("完成!!共耗时:{} m秒", (endTime - startTime));
  	@Override
    @Async("threadPoolTaskExecutor")
    public void asyncinsertUserDataList(UserData userData, CountDownLatch countDownLatch)
    {
        try  {
//            log.info("start executeAsync");
            userDataMapper.insertUserData(userData);
//            log.info("end executeAsync");
        } catch(Exception e){
            e.printStackTrace();
        } finally {
            // 无论上面程序是否异常必须执行 countDown,否则 await 无法释放
            countDownLatch.countDown();
        }
    }

补充-CountDownLatch 理论

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

创建一个 CountDownLatch 对象时,需要指定一个初始计数值,该计数值表示需要等待的线程数量。每当一个线程完成了其任务,它调用 CountDownLatch 的 countDown() 方法,计数器的值就会减一。当计数器的值变成 0 时,等待的线程就会被唤醒,继续执行它们的任务。

更详细的底层原理可以查看AQS相关知识

猜你喜欢

转载自blog.csdn.net/weixin_54594861/article/details/130442978