【问题标题】:Java How to make sure that a shared data write affects multiple readersJava如何确保共享数据写入影响多个读者
【发布时间】:2019-04-23 14:08:20
【问题描述】:

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

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

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

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

我假设当人们在这种情况下谈论缓存或缓存时,他们指的是处理器缓存(如果我错了,请纠正我)。

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

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

现在我有一些想法,但我不知道它们的成本有多大(很可能是):

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

  2. 在新的迭代开始之前,在同步块中为每个线程复制数组(最大可达到 195kB)

  3. 我对 ReentrantReadWriteLock 很感兴趣,但我不明白它与缓存有何关系。读取锁获取是否可以强制读取器的缓存更新?

【问题讨论】:

  • “我的问题是,如何确保在下一阶段 1 开始之前为工作线程更新数据(缓存)。” - 通常,您会等待执行更新的线程完成,然后开始另一个阶段 1。例如,您可以使用join method 来执行此操作。如果您还没有通读Java Tutorial on Concurrency,我建议您从那里开始。
  • 谢谢!教程结束了,刚刚。适合审查,但没有什么新东西。我知道如何同步线程,只是不知道如何保持内存一致性。如果我使数组易失,我假设它适用于引用而不是存储在数组中的数据。意味着我不知道存储在数组中的数据的任何更改是否对所有线程都是可见的。在我将数组处理为不可变的情况下,易失性会有所帮助,但我仍然必须在所有迭代之前制作价值 195kB 的数据的副本。或者我误解了一些非常糟糕的东西......

标签: java multithreading cpu-cache


【解决方案1】:

我在“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 点显示了四个不同线程同时通过随机(ish)读取对数组的一项进行“原子”访问和同步访问(同步 getter)之间的区别。这清楚地表明了“原子”访问的好处。

由于前四个值看起来高得惊人,我很想在没有多线程的情况下测量访问时间,所以我得到了第 5 点和第 6 点的结果。我尝试复制和修改之前测试中的方法,以制作代码尽可能接近。当然可以有一些我无法影响的优化。

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

如果更改了参数(例如数组的大小或运行的方法的数量),结果会有很大差异。

结论: 如果算法非常占用内存(来自同一个小数组的大量读取,被短计算中断 - 这是我的情况),多线程可以减慢计算速度而不是加快计算速度。但是如果它有很多读取,与数组的大小相比,使用数组的不可变副本并使用多个线程可能会有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-22
    相关资源
    最近更新 更多