java并发[一]探索获取合理的并发数

来源:互联网 发布:怎么制作apk软件 编辑:程序博客网 时间:2024/06/06 14:03

 背景介绍:

         最近技术调研,需要一次性解析大量的velocity模板,velocity解析做了大概的优化配置。其中,性能瓶颈主要涉及到批量Velocity模板解析和批量文件写入。为了满足效率要求,使用java.util.concurrent.Executors [JDK1.5 引入的线程池管理]管理并发,写了个工具类,采用固定数目的线程池,如下

public class ExecutorTaskHelper {      private ExecutorService executor;         private staticExecutorTaskHelper instance = new ExecutorTaskHelper();         private ExecutorTaskHelper() {         executor = Executors.newFixedThreadPool(AppConfig.getInstance().getIntValue("concurrence.threads.num", 10));     } /**      * 添加给定的任务,当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表。      * @param tasks 任务列表      * @param timeout 最长等待时间(单位:秒)      * @return表示任务的 Future 列表,列表顺序与给定任务列表的迭代器所生成的顺序相同。如果操作未超时,则已完成所有任务。如果确实超时了,则某些任务尚未完成。      * @throws InterruptedException 如果等待时发生中断,在这种情况下取消尚未完成的任务      */public static <T>List<Future<T>> addTask(Collection<? extends Callable<T>> tasks, int timeout) throws InterruptedException {         if(CollectionUtils.isNotEmpty(tasks)) {              return instance.executor.invokeAll(tasks, timeout, TimeUnit.SECONDS);         }         return null;     }}
 

1.      并发批量解析Velocity模板

其中,Velocity文本模板的行数达到3000多行,需要解析的数量一次性达到1000个以上。暂时不考虑velocity的解析优化。先从并发数测试触发,看看如何控制并发数。首先,将大概的解析方式罗列如下:

/**       * 并发解析Velocity文件       * @param factor 并发乘数因子       * @param templateContent 模板String       * @param contextMap 模板解析上下文map       * @throws InterruptedException       * @throws ExecutionException       * @throws IOException       */      public static long concurrencyParseContent(final float factor, final String templateContent, final List<Map<String, Object>>contextMaps) throws InterruptedException, ExecutionException,IOException {           LoggerUtil.logInfoEnabled(LOGGER, "The concurrent parsing start {}", new Date());           long start = System.currentTimeMillis();           //FIXME这个路径可能需要移到外部           final String filePath = AppConfig.getInstance().get("apm.template.content.store.path", "F:/temp");                     int poolNum = AppConfig.getInstance().getIntValue("concurrence.threads.num", 10);           int concurrencyNum = (int) (poolNum * factor);           List<Callable<Map<String,String>>> tasks = newArrayList<Callable<Map<String, String>>>(concurrencyNum);           //final Map<String, String>fileContentMap = new HashMap<String, String>(concurrencyOrgNum);           int index = 1;                     for (index = 1; index < contextMaps.size();index ++) {                 final Map<String, Object> contextMap =contextMaps.get(index);                 tasks.add( //使用匿名内部类                      new Callable<Map<String, String>>(){ //必须是一个接口或者类                                  public Map<String, String> call() throws Exception {                                       Stringresult = VelocityServiceUtil.mergeContentIntoString(templateContent,contextMap);                                       return MapUtil.newHashMap(filePath + "/filename" + IdUtils.uuid() + ".sh", result);                            }                      }                 );                 if (index % concurrencyNum == 0) {                      executeTasks(tasks);                      //重新填充任务                      tasks.clear();                 }                 /*if (index % concurrencyOrgNum == 0) {                      executeWriteFiles(fileContentMap);                      fileContentMap.clear();                 }*/ //不需要对存储做太多优化,可以降低内存使用率           }                     //最后一波处理任务           if (CollectionUtil.isNotEmpty(tasks)) {                 //executeWriteFiles(fileContentMap);                 executeTasks(tasks);           }           LoggerUtil.logInfoEnabled(LOGGER, "The concurrent parsing finish {}", new Date());           long useTime = System.currentTimeMillis()- start;           LoggerUtil.logInfoEnabled(LOGGER, "The use time {} ms", useTime);           return useTime;      }

如上,我们需要探讨的主要有三个参数,1. 解析数量,即contextMaps.size(),2. factor,这个是一个乘数因子,主要是为了方便能够在web应用中反复测试并发数,而不需要反复重启web工程,3. 线程池开启的固定线程数目poolNum。为了能找到性能优化的点,我采用物理中常见的控制变量法。

为了能够方便反复测试,写了个action,如下:

/** * @author linjx * @date 2014-8-21 * @version 1.0.0 */@ParentPackage("wsportal-default")public class TestConcurrentParseTemplateAction {           private float factor;      private int testNum;      private String message;       @Action(value = "/test/concurrent-parse",                 results= {@Result(name=BaseAction.SUCCESS, type="json")})      public String testParseTemplate() {           String template = null;           try {                 template = FileUtils.readFile("F:/velocity/msconf.vm");           } catch (IOException e) {                 e.printStackTrace();           }           ServerTemplateBo server = new ServerTemplateBo();           server.setIp("192.168.21.213");           server.setName("hello");           SoftwareTemplateBo software = new SoftwareTemplateBo();           software.setCacheType("4");           software.setConf("conf list");           software.setHasChildren(false);           software.setSquidId(1000000L);           software.setVersion("v1.0.0");                     Map<String, Object> contentMap= new HashMap<String, Object>(2);           contentMap.put("server", server);           contentMap.put("software", software);                     List<Map<String, Object>>contextMaps = new ArrayList<Map<String,Object>>(testNum);           for (int i = 1; i <= testNum; i ++) {                 contextMaps.add(contentMap);           }                     try {                 long useTime = VelocityConcurrencyUtil.concurrencyParseContent(factor, template, contextMaps);                 message = "总解析模板数: " + testNum + " "                            + "线程池线程数: " + poolThreadNum                            + "解析并发数: " + (factor * poolThreadNum) + "  "                            + "The use time " + useTime + " ms";           } catch (InterruptedException | ExecutionExceptione) {                 e.printStackTrace();           } catch (IOException e) {                 e.printStackTrace();           }                     return BaseAction.SUCCESS;      }           public String getMessage() {           return this.message;      }       public void setFactor(float factor) {           this.factor = factor;      }           public void setTestNum(int testNum) {           this.testNum = testNum;      }     }

我们只是使用了同一组数据,但是衍生出足够多的解析测试。

  

①  首先,测试一下解析的耗时和解析的数据量的关系

直觉上告诉我们,待解析的数目越多,耗时肯定也越多,而且应该是线性关系。我们做个测试,其中poopNum=10:

1.{"message":"总解析数: 100  并发因子: 1.0  The use time 1941 ms"}2.{"message":"总解析数: 1000  并发因子: 1.0  The use time 19700 ms"}3.{"message":"总解析数: 10000  并发因子: 1.0  The use time 192719 ms"}

结果显示,我们的判断是正确的。好的,那接下来就将contextMaps.size()值固定为500,这样做是为了避免CPU时间片轮转对测试造成波动,也可以避免数据量太大,导致测试等待太久,毕竟时间还是很宝贵的。

 

②变动factor并发因子,改变并发数(即一次性交给Executor的任务数)

一开始,我们开辟固定线程池,池内线程数固定为10个,然后不断修改factor,测试耗时如下

1. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 1.0  The use time 8285 ms"}2. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 2.0  The use time 6088 ms"}3. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 3.0  The use time 7252 ms"}4. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 4.0  The use time 8583 ms"}5. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 5.0  The use time 9762 ms"}6. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 8.0  The use time 10244 ms"}7. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 10.0  The use time 10533 ms"}8. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 20.0  The use time 10613 ms"}9. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 200.0  The use time 8728 ms"}10. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 500.0  The use time 8943 ms"}

通过上述数据,我们可以发现,任务的并发执行数并非越大越好,也不是越小越好,我反复测试发现,并发数为2是效率最高的。这是为什么呢?我仔细查看了,我的CPU是Intel i3,具体参数如下

核心类型

Arrandale

核心数量

双核心

热设计功耗(TDP)

35W

制作工艺

32 纳米

晶体管数目

3.82亿

核心面积

81平方毫米

封装模式

rPGA 37.5×37.5mm, BGA 34×28mm

我一直以为i3已经是四核了,可是不是,就双核而已;刚好和我们测试的结果是一致的。这样就不难解释了,虽然我们使用Executors开辟了10个固定的线程,如果我们一次性加载比较多的任务,根据操作系统的执行方式,使用Intel i3肯定是无法一次性就执行这么多任务,最多只能双核一起跑,即真正的同时执行两个线程(感觉,最终面向对象的执行还是很难逃脱面向过程的)。如果任务数大于实际真正的并发数,那在某个任务没有结束时,该任务会被挂起,等待下一个时间片轮转到它。这样,从某种程度上讲,CPU等于同时要处理多个任务,可是就只有两颗心,唯一能做的,就是分神,一边一边咯。自然,虽然CPU拥有很快的计算速度,但是还是会一定程度降低运行效率。除非CPU只做解析Velocity这件事,这样时间的耗散就理论上一致了——可惜,这是不可能的。
总结到这里,不禁联想到最近看的MapReduce。单机的计算能力终究是有限的,就像一个工人不可能单独一日完成一车间的任务,不管是横向的重复性任务还是纵向的流水线任务。MapReduce就像是集结一个车间的工人,分布式的使用了计算机集群。从这个优秀的框架我们可以看到车间流水线的影子。Map workers和Reduce workers分别负责分布式运算的流水任务,Reduce workers在Map workers完成第一轮的流水线任务之后就可以陆陆续续地开始自己的任务(如果有一节点既负责Map又负责Reduce任务,那还是需要串行执行的)。相同身份的workers是横向分布的,他们处理相同的任务,是单兵能力的有效扩展。备注:我对MapReduce算法解刨层面的理解是,对应数学上的微积分,一个先离散再聚合的过程,key对应的是微分参数。具体的理解,希望回头有机会实战后能给个有意义的讲解。至此打住,跑题太远。
回到正题,既然我们只能够真正同时执行两个线程,那线程池是不是也没有必要开辟那么大。毕竟我这里处理的方式是,执行完成一个任务集合后,在继续执行下一个任务集合,或者加入一堆的任务,由Executor自己分配。所以,如果这样,是不是直接把线程池开辟为2个就会提升效率呢?试试看(为了方便测试,poolNum也交由前端输入,具体代码小修改不做展示):
1.{"message":"总解析模板数: 500  线程池线程数: 1  解析并发数: 1.0  The use time 12461 ms"}2. {"message":"总解析模板数: 500  线程池线程数: 2  解析并发数: 2.0  The use time 8717 ms"}3. {"message":"总解析模板数: 500  线程池线程数: 4  解析并发数: 4.0  The use time 11437 ms"}4. {"message":"总解析模板数: 500  线程池线程数: 10  解析并发数: 10.0  The use time 10623 ms"}5. {"message":"总解析模板数: 500  线程池线程数: 100  解析并发数: 100.0  The use time 10233 ms"}
(这测试过程中,我发现,由于本机有时候某些后台程序释放了资源,计算能力会提升,所以有时候计算能力会提升,所以,数据比对只在一个时间段内做比对,不跨时段)
上述,我们不过内存损耗的开辟比较大的线程池,结果如预计的那样,并没有提高解析速度。就此,我们可以认为,单纯的提交解析效率,合理的并发数应该是CPU可真正同时执行的线程数。
 
0 0
原创粉丝点击