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可真正同时执行的线程数。
- java并发[一]探索获取合理的并发数
- 关于JAVA多线程并发synchronized的测试与合理使用
- 关于JAVA多线程并发synchronized的测试与合理使用
- 关于JAVA多线程并发synchronized的测试与合理使用
- JAVA中的并发工具类(一)----控制并发数的Semaphore
- (java多线程并发)控制并发线程数的Semaphore
- java并发编程的艺术【一】并发编程的挑战
- Java并发编程(一):并发编程的挑战
- JAVA 并发类(一) 常用的并发类
- java并发编程学习(一) 并发编程的挑战
- 并发数的计算
- 并发数的计算
- 探索并发编程(一)------操作系统篇
- 探索并发编程(一)------操作系统篇 .
- 探索并发编程(一)------ 操作系统篇
- 探索并发编程(一)------操作系统篇
- 探索并发编程(一)------操作系统篇
- 探索并发编程(一)------操作系统篇
- Leftmost Digit
- Chap2 Insertion Sort
- 查看Eclipse版本号及各个版本区别 .
- 虚拟存储器
- 今天真是郁闷,,
- java并发[一]探索获取合理的并发数
- grep
- Sudoku Solver
- WARNING: Waited 15 secs for write IO to PST disk 4 in group 3 in alert_asm.log
- Java基础-java分类
- 算法设计之归并排序(C++实现)
- Eclipse工具栏上添加Android SDK and AVD Manager项
- 黑马程序员—[.Net就业薪资] 黑马.Net 12期毕业33个工作日,就业率达98%,平均薪水:6972元
- 黑马程序员—[Android就业薪资] Android27期,毕业57天,100%全部就业,平均薪水8815元!