java - 消失的可扩展性

标签 java concurrency

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

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

但是,当我取消注释第 44~53 行(该行在本地进行简单计算)并将变量 s 调整为足够大的值时,可扩展性可能会消失。

我的问题是我的玩具程序是否使用共享数据,这可能会导致并发性能下降。如何解释消失的可扩展性(我认为低级开销,如垃圾收集,导致了这一点)?有什么办法可以解决像这样的问题吗?

最佳答案

有问题的代码是:

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 中很好地扩展并发性,无论你的任务是大还是小,你都必须注意如何使用内存,意识到潜在的隐藏陷阱和低效率,并优化掉低效部分。

关于java - 消失的可扩展性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9302908/

相关文章:

java - 使用 Liberty 配置文件配置 log4jdbc-log4j2

java - 错误 : Failed to resolve: androidx. 应用程序兼容性 :design:1. 1.0

c# - 当锁没有竞争时, lock(...) 的开销是多少?

java - java同步中Read Lock的用途是什么

java - Java volatile 是阻止缓存还是强制执行直写缓存?

Java - 分配的空间未减少

java - HTML 嵌入不适用于 Java Applet

java - JTextPane 背景颜色

c - C 中的模拟并发

Java 并发 : CAS vs Locking