【问题标题】:How Java manages the multithread access to elements of arrays?Java 如何管理对数组元素的多线程访问?
【发布时间】:2011-07-03 14:32:19
【问题描述】:

各位程序员大家好。我已经问过one question,但尽管我得到了非常好的答案,但我无法解决我的问题。 然后,我花时间重构我的代码,以提高其并行化潜力(通过更少的计算批次和更多的计算任务)。但我仍然无法获得比串行处理更好的性能。

我怀疑这种缓慢的并行处理是由于上下文切换。或者可能是由于普通对象的“自动”同步。我想你可以帮助我了解发生了什么。

让我陈述一下我的情况:我正在编写一个用于科学计算的程序。它不依赖于外部事物,只依赖于我在开始时给它的输入值。 这个问题的大小可以用Ns(我用的这个名字)来衡量。可以看成是解的“分辨率”,是用户输入的一种,通常在100左右。

这样,我的主类中有几个双精度数组,例如双精度 ys[Ns][N]phiS[Ns][Nord][N],其中 N 和 Nord 是程序的其他固定量值。 在我的程序中,我必须为每个Ns 点计算几件事,然后并行化。每个点计算都是独立的,所以我可以将它们分成不同的线程,希望它变得更快。

因此,我没有使用循环for (int i=0; i<Ns; <i++),而是将此计算任务划分为可运行批次,每个批次都在一个较小的区间内:for (int i=start; i<end; i++),其中开始和结束始终介于 0 和 Ns 之间。例如,如果我在双核电脑上,我会制作两批,一批是start = 0end = Ns/2,另一批是start = Ns/2end = Ns。如果我在四核上,第二批将有 start = Ns/4end = Ns/2 等等(假设在每种情况下划分都是准确的)。

每个 Batch 作为实现 Runnable 的类,存储在 ArrayList<Batch> 中,并分配给大小等于内核数的 FixedThreadPool。它使用简单的CountDown 方案执行批处理并等待它们完成。

每个批次都需要从程序的主类访问这些数组上的数据,但是它们的访问使得每个批次只能从yS[start][] 读取到yS[end][],因此两个批次永远不会尝试读取相同的数组元素。我想知道 Java 是否仍然锁定 yS,即使每个批次都没有尝试访问与其他批次相同的元素。

我还想知道我的问题是否与上下文切换导致的开销有关,因为每个批次需要处理数千个双精度数,以及程序的构建方式是否会影响它。

也许我应该找到一种方法将与其相关的数组元素传递给每个批次,但我不知道如何处理这个问题。如果有指针,我可以通过简单的指针操作获得仅包含所需元素的新数组,而无需重新分配任何内容。有没有办法在 Java 中做这样的事情?

好吧,最后,提一下:有一部分代码需要同步(它处理其他数组)并且它已经可以正常工作了。 我上面描述的计算任务并不是我的程序唯一要做的事情。它们在一个循环中,与顺序处理部分交替,但作为总执行时间非常重要。

所以,总而言之,问题是:为什么我没有从多线程中获益,而我原本期望的是?

我刚刚在这里运行了几次普通串行程序和多线程程序,串行程序为 14500 毫秒,多线程程序为 15651 毫秒。两者都在同一个双核上。 其他需要注意的点:在串行运行中,每个计算任务(从 0 到 Ns)大约需要 1.1 到 4.5 ms。 从双线程开始,每批(Ns/2 个点)大约需要 0.5 到 3 毫秒; (从run()方法的上到下测量。每次计算任务因自己的数值收敛而异)

非常感谢您的关注。

【问题讨论】:

  • 原来的用了多长时间,你的新程序用了多长时间?
  • 您是否运行了分析器?
  • @Paŭlo:普通串行程序需要 14500 毫秒。双核并行需要 15651 毫秒
  • @Mark:了解发生了什么,为什么我没有在我期望的时候获得多线程。
  • @Micheal,抱歉,您所说的分析器是什么意思?我不是很有经验的程序员,这是我第一次体验多线程。

标签: java multithreading synchronization context-switch


【解决方案1】:

您可能遇到的一种可能是线程在缓存行上颠簸。如果不同的线程快速写入同一缓存行中的位置(例如,在同一数组中关闭),则硬件具有很高的通信开销以确保数据保持一致。

【讨论】:

  • 一个正确的答案,但我怀疑 OP 理解缓存行。
  • 也许...但是当一个线程读取 yS[0] 时,另一个线程读取 yS[50]。然后,第一个读取 yS[1] 和另一个 yS[51] 等等......
  • @ursoouindio,哇,你有一个没有序列化的多线程应用程序。简单地说:没有“那么”部分。双数据类型 yS[0] 和 ys[7] 将位于同一字节缓存行上(x86_64 缓存行为 64 字节)。回答您的难题:将数组平均分成相等的部分。 IE。如果你有 int[2048],第一个从索引 0 开始,第二个从索引 1024 开始。永远不要在多线程应用程序中进行关闭索引。 {我会找一些关于假隔离的好文章)
  • 哎哟!对不起,@bestsss。你说的对,呵呵。但是现在我关注缓存的东西,但不知道如何处理它。如果你找到一篇关于它的好文章,请告诉我!谢谢
  • 这里:2 篇文章。它不是 java,但语言 (Java/C/C#) 没有任何区别。 bluebytesoftware.com/blog/2009/10/20/FalseSharingIsNoFun.aspx(作者是 .NET 架构师)和规范 - 维基百科:en.wikipedia.org/wiki/False_sharing 然而,这是一篇薄弱的文章。
【解决方案2】:
 I wonder if Java still locks up yS, even that each batch isn't trying to access
 the same elements as others.

Java 中没有自动同步或锁定。你必须明确编码。

I wonder also if my problem is related to the overhead due to context switching..

上下文切换确实有开销。如果您的所有线程都在同一个任务上工作,这是 CPU 密集型的,那么您的线程数应该等于可用处理器内核的数量。

If there were pointers, I could have new arrays of just the desired elements with
simple pointer operations and without reallocating anything.

Java 中的所有对象都是通过引用传递的(例如,当您将它们传递给方法时)。基本上所有的引用都是指针(不同的是你不能取消引用它们)。因此,在 Java 中不会复制任何对象,除非您的代码明确要求。

话虽如此,您应该注意另一件事:如果您向集合(列表、HashMap 等)添加大量元素,则集合需要增长。在内部,所有 Collections 都使用数组来存储元素,当添加元素时,需要调整数组的大小。由于无法在 Java 中调整数组的大小,因此需要创建一个新数组并将对旧对象的所有引用复制到一个新数组中。或者,如果您使用原始类型,则需要复制所有数据。因此,在创建集合时,您应该将它们调整为适当的大小,这样就不需要调整它们的大小。

您可能还想阅读How many threads should I use in my Java program?

【讨论】:

  • 谢谢@Peter,但我很清楚,最佳线程数是处理核心数。我就是这样用的。并感谢您对自动锁定的启发。
  • 当我提到指针时,我想如果我传递从yS[start][] 开始并以yS[end][]elements 结束的数组,而不是整个 yS[ ][]...有意义还是没有区别?
  • 没有区别,因为您只传递对数组的引用,而不是数组本身。所以只创建了一个新的引用,而不是一个新的数组。
  • 我明白了。感谢您提供此信息。我认为也许每个寻求相同数组的可运行对象都是缺乏性能改进的原因。
【解决方案3】:

根据您到目前为止提到的内容,我会尝试以下操作

  1. 比较串行和并行版本的结果,以增加阵列的大小。性能差异对于您的问题大小确实可能微不足道,并且可能仅在大小变大(即数组大小)时才显示出来

  2. 为每个可运行对象提供其自己的数组副本。鉴于性能,数组在内存中的布局方式以及访问它们的方式都会对性能产生影响。即使您可能有一个 2D 数组,它也会被布置为内存中串行的并发数组列表。因此,如果您在 runnable 之间共享此数组,则其中一些可能会变得低效。

【讨论】:

  • 我测试过的第一个。到目前为止,它刚刚开始在比我需要的高得多的 Ns 值下变得更快,但我相信即使对于更小的 Ns 也应该显示出改进......
  • 第二个:你的意思是传递数组的引用(通常的事情),然后将其存储在可运行文件中(就像this.yS = ySpassedFromArgument)?还是在每次出现计算任务时,在可运行的局部变量中制作本地副本?
  • 是的,我的意思是建议一个本地独立副本。复制会以负面的方式影响性能,但如果较小的副本更有效地布置在内存中,则可能会抵消每个可运行文件
  • 四处搜索 (ibm.com/developerworks/java/library/j-5things4.html?ca=dat-),我找到了那些 CopyOnWriteArrayLists,但我并没有弄清楚它们到底是如何工作的。是这样吗?
  • 值得一试。谢谢,@Danish。
【解决方案4】:

您是否有足够的可用内存来创建多个集合并将唯一的工作集合传递给每个线程,这样您就可以完全消除多个线程访问同一内存的争用?

【讨论】:

  • 是的,我想内存就足够了。但由于这个程序只是在更大的应用程序中使用的一部分,我想保持它的内存使用量很小......但值得一试。你建议如何处理这个问题?
  • 这会使处理器缓存问题更加严重。高性能计算的全部内容是防止缓存刷新和重新加载。为了最小化你想要最小化你操作的数据集并避免重复副本。
  • @Peter,实际上我做了类似的事情(按照丹麦人的建议),问题并没有变得更糟,但性能没有改变。
  • @Peter 我会买这个,将多个指针指向内存的同一部分肯定看起来更接近芯片,因为更多的东西正在请求它。
猜你喜欢
  • 2012-03-08
  • 1970-01-01
  • 1970-01-01
  • 2020-11-19
  • 2021-11-21
  • 1970-01-01
  • 1970-01-01
  • 2016-03-11
  • 1970-01-01
相关资源
最近更新 更多