消失的可扩展性

时间:2012-02-15 23:04:05

标签: java concurrency

我制作了一个玩具程序来测试Java的并发性能。我把它放在这里: https://docs.google.com/open?id=0B4e6u_s5iHT6MTNkZGM5ODQtNjZmYi00NTMwLWJlMjUtYzViOWZlMDM5NGVi

它接受一个整数作为参数,指示要使用的线程数。该程序只是计算出一个范围内的素数。通过注释第44~53行获得通用版本,它产生了几乎完美的可伸缩性。

然而,当我取消注释第44~53行(在本地进行简单计算)并将变量s调整到足够大的值时,可伸缩性可能会消失。

我的问题是我的玩具程序是否使用可能导致并发性能下降的共享数据。以及如何解释消失的可伸缩性(我认为低级开销,如垃圾收集,会导致这种情况)?任何解决方案都可以解决这种情况的问题吗?

1 个答案:

答案 0 :(得分:2)

有问题的代码是:

int s = 32343;
ArrayList<Integer> al = new ArrayList<Integer>(s);
for (int c = 0; c < s; c++) {
    al.add(c);
}
Iterator<Integer> it = al.iterator();
if (it.hasNext()) {
    int c = it.next();
    c = c++;
}

当然,如果您增加s的值,这会降低性能,因为s会控制您放入列表的内容。但这与并发性或可伸缩性几乎没有关系。如果您编写的代码告诉计算机浪费时间进行数千或数百万次丢弃计算,那么您的性能当然会降低。

在更多技术术语中,此部分代码的时间复杂度为O(2n)(构建列表需要n个操作,然后n操作迭代它并增加每个值),其中n等于s。因此,s越大,执行此代码所需的时间就越长。

就为什么这似乎会使并发性的好处变小,您是否考虑过s变大的内存含义?例如,您确定Java堆足够大,可以将所有内容保存在内存中而不会将任何内容交换到磁盘吗?即使没有任何东西被换出,通过使ArrayList的长度变大,你在垃圾收集器运行时会给它做更多的工作(并且可能增加它运行的频率)。请注意,根据实现情况,垃圾收集器可能会在每次运行时暂停所有线程。

我想知道,如果你在创建线程时为每个线程分配一个ArrayList实例,然后在调用isPrime()时重用它,而不是每次创建一个新列表,这会改善一些事情吗?

修改:以下是修正版本:http://pastebin.com/6vR7Uhez

它在我的机器上提供以下输出:

------------------start------------------
1 threads' runtimes:
1       3766.0
maximum:    3766.0
main time:  3766.0
------------------end------------------
------------------start------------------
2 threads' runtimes:
1       897.0
2       2483.0
maximum:    2483.0
main time:  2483.0
------------------end------------------
------------------start------------------
4 threads' runtimes:
1       576.0
2       1473.0
3       568.0
4       1569.0
maximum:    1569.0
main time:  1569.0
------------------end------------------
------------------start------------------
8 threads' runtimes:
1       389.0
2       965.0
3       396.0
4       956.0
5       398.0
6       976.0
7       386.0
8       933.0
maximum:    976.0
main time:  978.0
------------------end------------------

...随着线程数量的增加,它显示出几乎线性的缩放。我修复的问题是上面和John Vint(现已删除)答案中提出的点的组合,以及ConcurrentLinkedQueue结构的错误/不必要的使用以及一些可疑的时序逻辑。

如果我们启用GC日志记录并对两个版本进行配置,我们可以看到原始版本运行垃圾收集的时间大约是修改版本的10倍:

Original:  [ParNew: 17401K->750K(19136K), 0.0040010 secs] 38915K->22264K(172188K), 0.0040227 secs]
Modified:  [ParNew: 17024K->0K(19136K),   0.0002879 secs] 28180K->11156K(83008K),  0.0003094 secs]

这对我来说意味着在常量列表分配和Integer自动装箱之间,原始实现只是在过多的对象上搅动,这会给GC带来太多负载,从而降低了线程的性能。指出创建更多线程没有任何好处(甚至是负面好处)。

所有这一切对我来说,如果你想在Java中很好地扩展并发性,无论你的任务是大还是小,你都必须注意你如何使用内存,要注意潜在的隐藏陷阱和低效率,并优化低效率。