Java如何确保共享数据写入会影响多个读取器

时间:2018-11-21 03:06:36

标签: java multithreading cpu-cache

我正尝试编写处理器密集型任务的代码,因此我想使用多线程并在可用处理器内核之间共享计算。

假设我有数千次迭代,所有迭代都有两个阶段:

  1. 一些正在扫描成千上万个选项的工作线程 他们必须从共享数组(或其他数据结构)中读取数据,而无需修改数据。
  2. 一个线程从所有工作线程中收集结果(同时 他们正在等待)并在共享阵列上进行修改

阶段是按顺序进行的,因此没有重叠(不会同时写入和读取数据)。我的问题是:在下一个阶段(阶段1)开始之前,如何确定工作线程的数据(缓存)已更新。

我假设当人们在这种情况下谈论缓存或缓存时,它们的意思是处理器缓存(如果我错了,请修复我)。

据我了解,volatile只能用于非引用类型,而没有使用同步的意义,因为工作线程在读取时会相互阻塞(在处理一个选项时可能有数千次读取)。

在这种情况下我还能使用什么?

现在我有一些想法,但是我不知道它们有多昂贵(很可能是):

  1. 为所有迭代创建新的工作线程

  2. 在一个新的迭代中,在每个新线程开始之前,为每个线程制作一个数组副本(大小最大为195kB)

  3. 我很喜欢ReentrantReadWriteLock,但我不明白它与缓存有何关系。读取锁是否可以强制读取器的缓存进行更新?

1 个答案:

答案 0 :(得分:0)

我正在寻找的东西在“ Java并发教程”中提到,我只需要更深入地了解。在这种情况下,它是AtomicIntegerArray类。不幸的是,它的效率不足以满足我的需求。我进行了一些测试,也许值得分享。

我估算了不同内存访问方法的成本,方法是多次运行它们并平均经过的时间,将所有内容分解为一次平均读取或写入。

我使用大小为50000的整数数组,并在每种测试方法中重复了100次,然后取平均结果。读取测试正在执行50000次随机(ish)读取。结果显示一次读/写访问的大概时间。尽管如此,这不能说是精确的度量,但我相信它可以很好地了解不同访问方法的时间成本。但是,对于不同的缓存大小和时钟速度,在不同的处理器上或使用不同的数字时,这些结果可能完全不同。

结果是:

  1. 设置的填充时间为:15.922673ns
  2. 使用lazySet的填充时间为:4.5303152ns
  3. 原子读取时间为:9.146553ns
  4. 同步读取时间为:57.858261399999996ns
  5. 单线程填充时间为:0.2879112ns
  6. 单线程读取时间为:0.3152002ns
  7. 不变的复制时间是:0.2920892ns
  8. 不变的读取时间是:0.650578ns

第1点和第2点显示了在AtomicIntegerArray上进行顺序写入的结果。在某些文章中,我谈到了lazySet()方法的良好效率,因此我想对其进行测试。通常会将set()方法执行大约4次,但是不同的数组大小会显示不同的结果。

第3点和第4点显示了通过四个不同线程同时进行的随机读取对数组的一项进行“原子”访问和同步访问(同步吸气剂)之间的区别。这清楚地表明了“原子”访问的好处。

由于前四个值令人震惊,我真的很想在没有多线程的情况下测量访问时间,所以我得到了第5点和第6点的建议。我试图复制和修改先前测试中的方法,以编写代码尽可能接近。当然,可以有我不能影响的优化。

然后出于好奇,我提出了第7点和第8点,它们模仿了不变的访问方式。这里一个线程(通过顺序写入)创建数组,并将其引用传递给另一个线程,该线程对该数组进行随机(ish)读取访问。

如果更改了参数(例如数组的大小或正在运行的方法的数量),结果将有很大的不同。

结论: 如果算法占用大量内存(从同一个小数组中读取大量数据,但由于简短的计算而中断-这是我的情况),那么多线程处理可能会减慢计算速度,而不是加快计算速度。但是,如果读取次数很多,与数组的大小相比,使用数组的不变副本并使用多个线程可能会有所帮助。