多线程的作用
并发执行任务,快一点。为什么可以并发执行多个任务?因为现在计算机都是有多个cpu。
应用场景
一种是子任务可以异步执行。比如,支付的时候,支付成功之后,异步发mq消息通知商户。这里的发mq消息通知商户就是子任务,并且是可以异步。那么就可以启一个线程去让这个子任务慢慢的执行,至少是不用阻塞主任务——这样的话,客户端就可以更快的得到调用结果,即支付速度更快了。
一种是并发执行多个任务。比如上面的作用那一节里写的。但是其实并发执行本质上就是异步,比如,web服务器都是线程池,每个请求都有单独的线程去处理,为什么web服务器要用线程池?不就是为了启一个线程异步处理请求任务吗,不就是为了不阻塞主线程吗。
如何实现异步?
从上文看,最核心的关键字是异步,如何实现异步?就是启一个子线程去执行任务,启一个子线程的本质就是异步执行任务,因为子线程的执行不会影响其他线程,包括主线程。
所以,主线程可以不断的启子线程异步处理更多的任务。
哪怕是子线程异常了,也不影响其他线程。
那具体用什么技术实现呢?线程池。
线程池
线程池是一个概念,具体的技术就是Executor。再具体一点其实就是创建一个线程池,然后启一个子线程去执行任务。
创建一个线程池
privatestaticExecutorServiceexecInsert=newThreadPoolExecutor(20,150,5,TimeUnit.SECONDS,newArrayBlockingQueue<Runnable>(600),newThreadPoolExecutor.CallerRunsPolicy());
创建线程池的时候,就用这个ThreadPoolExecutor,也没有必要用其他的,阿里巴巴规范手册都说了要用这个,不用用其他的。
阿里巴巴规范手册
阿里巴巴为什么这么建议?主要就一个原因,有内存泄露的可能。为什么可能内存泄露?因为线程数量默认是无限大,请求队列大小默认也是无限大。
所以,大部分情况,都是直接用ThreadPoolExecutor这个就可以了。没有必要用其他的,其他的就是指:用Executors去创建阿里巴巴规范里提到的那几种没有配置实际大小参数的连接池。
类继承图
说明:一般情况下,就用Executor就可以了。除非是需要获取子线程的执行结果,才需要用到ExecutorService,另外ExecutorService还增加了线程池的生命周期管理功能。
ThreadPoolExecutor是具体的实现,大部分情况,直接用ThreadPoolExecutor就可以了。
是否需要获取子线程的执行结果?
因为一般情况下,启一个子线程去执行任务,就不管了,也不需要管。所以直接用Executor就可以了,即任务类实现Runnable接口。看截图:如果不需要获取子线程的执行结果,就用Runnable,即子线程任务类实现Runnable接口。
如果需要获取线程执行结果,一种是执行成功,一种是执行异常。这个时候就需要获取执行结果,并且必须阻塞等到子线程执行成功,主线程才继续往下执行。看截图:如果主线程需要获取子线程的执行结果,就用Callable,即子线程任务类实现Callable接口。
什么应用场景会需要获取子线程的执行结果呢?就是主线程后面的代码依赖子线程的执行结果,如果子线程执行成功,主线程是一种执行逻辑;如果执行失败,主线程又是另外一种逻辑。这个时候就需要获取子线程的执行结果。
比如:
publicRouteResultroute(RouteParamparam){LogUtil.LOGGER.info("route入参参数为{}",param);longstartTime=System.currentTimeMillis();if(!TranType.FORWARD.getIndex().equals(param.getTrantype())&&!TranType.REVERSE.getIndex().equals(param.getTrantype())){thrownewBizException("trantype传值错误,实际传值:"+param.getTrantype());}//参数检查StringseqId=String.valueOf(SnowflakeIdWorker.generateId());param.setRouteGroupKey(seqId);RouteResultrouteResult=null;if(StringUtils.isBlank(param.getDbName())){//进行路由routeResult=routeService.routeRule(param);}else{routeResult=newRouteResult();routeResult.setDbName(param.getDbName());}routeResult.setId(seqId);//保存路由信息QrcodeRouteInforouteInfo=initRouteInfo(param,routeResult);//写分库数据到mongodbBooleanaBoolean=routeInfoService.saveRouteInfoMongo(routeInfo);//保存mongodb,后续看是否需要改成异步//异步写分库数据到数据库Callable<String>call=newCallable<String>(){@OverridepublicStringcall()throwsException{longstartTime=System.currentTimeMillis();//开始执行耗时操作routeInfoService.inserRouteInfo(routeInfo);longendTime=System.currentTimeMillis();return"ok";}};Stringseoul=null;try{Future<String>future=execInsert.submit(call);seoul=future.get();//阻断获取子线程的执行结果//get必须要捕获异常,否则影响主线程继续执行。如果不捕获,编译也通不过。}catch(Exceptione){LogUtil.LOGGER.error("获取返回异常",e);}LogUtil.LOGGER.info("future.get()result:{},aBooleanis:{}",seoul,aBoolean);//如果写mongodb和数据库同时异常,则阻断交易,否则渠道回调的时候找不到原交易if(!"ok".equals(seoul)&&!aBoolean){log.error("写mongodb和数据库同时异常");thrownewBizException("写mongodb和数据库同时异常");}longendTime=System.currentTimeMillis();LogUtil.LOGGER.info("route返回对象:{},执行耗时:{}ms",routeResult,endTime-startTime);returnrouteResult;}
从上面代码和注释可以看到,如何获取Callable的执行结果,用get方法,get方法是阻塞,现在主线程虽然启了一个子线程,但是主线程现在实际上是同步,因为get是阻塞获取执行结果,不是异步。即便主线程不是异步,但是子线程是异步执行,因为主线程会启动多个子线程去同时执行多个子任务,所以还是有一定的优化作用。
Runnable和Callable的区别?
Callable适合需要知道结果的场景。最佳实践是,如果没有更多的需求,只是启一个线程,让这个线程去执行任务,就没有必要用Callable,因为简单一点。
如果主线程需要知道子线程的执行结果,那么就用Callable,因Callable可以获取子线程的执行结果。
子线程执行任务的时候异常,抛到哪里去了?该如何处理?
子线程异常了,就抛出异常,可能会打印异常日志,但是不会影响其他子线程,因为各个子线程是独立的。
抛到哪里去了?就在子线程里。
该如何处理?不需要处理,因为不会影响其他子线程。
但是,如果用Callable并且用get获取执行结果(实际上,只有用get才需要用Callable,不然就没有必要用Callable),那么主线程就必须要catch get异常,因为get会获取子线程的执行结果,这个结果包括正常结果和异常结果(即异常了)。所以主线程必须要捕获,不然会影响主线程继续执行后面的代码。
参考:https://www.cnblogs.com/thisiswhy/p/13704940.html
线程池数量配置
公式:cpu的2倍。
但是实际上,一般都会比cpu的2倍多一点。
比如,core线程数量理论是10,但是实际上可能是20。
数组阻塞队列一般是1000左右。
最大线程数量理论是cpu的2倍20,但是实际上可能是50,甚至100。
超过core数量的闲置线程,最大闲置时间一般是几s,比如5s或10s,超时之后就会被回收。
keepAliveTime–whenthenumberofthreadsisgreaterthanthecore,thisisthemaximumtimethatexcessidlethreadswillwaitfornewtasksbeforeterminating.//jdk官方解释unit–thetimeunitforthekeepAliveTimeargument
生产配置
privatestaticExecutorServiceexecInsert=newThreadPoolExecutor(20,150,5,TimeUnit.SECONDS,newArrayBlockingQueue<Runnable>(600),newThreadPoolExecutor.CallerRunsPolicy());
美团生产的配置,最大数量50。
是否有必要使用多个线程池?
没找到中文资料。
在公司zf的生产实践是,job应用里的每个job搞了一个线程池,每个线程池最大10个线程。n个job,加起来也有几十个线程。
在公司ys的生产实践,job应用也是一样,有多个job,每个job用独立的线程池,每个线程池最大20个。交易也是多个线程池,光是发mq就有多个线程池,因为支付成功之后,要给多个系统发通知。所以,可以有多个,但是不需要太多,而且每个线程池数量不要太大。
总结,一般情况下,一个线程池,其实是最好的,因为简单,而且速度快。为什么快?因为多个线程池,管理起来也耗资源。
但是如果有业务场景需要,比如多个job,可以每个job用独立的线程池,然后每个线程池用2倍cpu即可。
参考:https://stackoverflow.com/questions/60465783/what-happens-when-a-single-program-has-multiple-threadpoolexecutors
java并发编程实战推荐不同的任务,用不同的线程池,因为线程池只有在任务一样的情况下,才会执行效率最高。比如任务A和任务B的执行时间不一样,耗时长的任务A,可能占了很多线程,导致耗时短的任务B执行时间增加。说白了,就是任务一样,就用一个线程池,因为只有任务一样,线程池的效率才是最高的,不会出现耗时长的任务A导致耗时短的任务B耗时增加。
https://stackoverflow.com/questions/26243422/is-having-a-single-threadpool-better-design-than-multiple-threadpools
https://doc.zeroc.com/ice/3.7/client-server-features/the-ice-threading-model/thread-pool-design-considerations
参考
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html