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

 背景介绍:

         最近技术调研,需要一次性解析大量的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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值