我正在尝试用Java编写一个actor实现。我的设计需要一个高性能的地图数据结构来用于查找特定演员的预定线程。查找是使用int id完成的。所有演员都有独立的ID。我有以下要求:
i)键是原始注入,而不是整数类。
ii)值也是原语。值只能涵盖在实例化数据结构之前已知的几个数字。值只是线程/核心的id,因此它可以是 short 。线程数小于机器上的核心数,因此无法真正达到很高的数量。
iii)地图由单线程写入,但是从多个中读取。我希望我的实现是无锁的,没有任何共享(错误或其他)。因此,读取不应涉及对非线程本地存储器的任何写入。
iv)使用地图进行查找的多个读取器线程的读取数量将远远超过单个线程的写入次数。
v)所需的主要操作:
Set(key, value)
和delete(key, value)
。设置的大多数键最终也会被删除,因此大量删除后的性能不会降低。将使用递增整数生成新键(actor-ids)并表示创建actor。删除键(演员的id)表示该演员退出并且永远不会复活。这也意味着一旦被删除的密钥永远不会被再次设置。重要的是我们不会在地图中累积死区,因为删除将会发生(演员退出)。
Get(key)
从读者线程调用。
vi)操作get(key)
需要eventually consistent
但有一些警告。假设编写者线程已将对key1-> value1更改为key1-> value2。如果其中一个读者执行get(key1)并仍然接收value1,则不成问题。最终它应该得到value2。如果编写器线程删除了对key1-> value1,并且读取器线程上的get(key1)仍然返回value1,那也没关系。在实践中,我的意思是可以合并诸如Java putOrderedObject/lazySet/getObjectVolatile
或C ++ 11的std::memory_order_relaxed/std::memory_order_acquire/std::memory_order_release
之类的东西。另一方面,get(key1)
如果确实设置了值,则不应返回空值(比如-1)。如果getStrict(key1)
返回空值以满足此要求,我不介意进行get(key1)
操作。
我没有立即使用库的原因是:
i) Java集合:它们需要包装类,因此不符合我的目标(i)和(ii)
ii) Trove,FastUtil 等:它们确实有原始地图,但不提供任何并发访问设施。它们也没有针对稀疏范围内的值进行优化 - 在我的情况下是核心数。
iii) Java ConcurrentHashMap / ConcurrentSkipListMap :它们需要包装类,不针对单个编写器,多个读者用例进行优化。
我意识到这些是很多要求,所以如果答案解决了一些问题,同时对其他问题保持模棱两可,那就没问题了。指向我的设计源/代码或评论会很棒。任何对权衡的解释都会有额外的好处,因为我正在努力学习如何钓鱼。
如果我将我的详细要求归结为几个问题,可能是:
i)如何针对单作者/多读者用例进行优化?
ii)如何设计get(key)
和getStrict(key)
操作?这是考虑它的正确方法吗?
iii)如何设计我的地图以利用递增键和稀疏值范围?
iv)如何以最佳方式处理频繁删除?任何调整大小/重新散列都需要立即对读者线程可见,而不是最终可见。
欢迎使用C ++ / C ++ 11代码的任何答案。通过一些研究,我应该能够将大多数std :: atomic操作转换为Java不安全操作。
答案 0 :(得分:2)
虚假分享只来自多个作家,因为你有一个作家,你不应该在作家之间分享问题。
i)如何针对单作者/多读者用例进行优化?
您不需要为多个读者做任何特殊操作,每个线程都会有一个数据结构的本地副本。单个编写器是最简单(也是最快)的用例。
因此,Trove和ConcurrentMaps都做得很好。 BTW ConcurrentMap也针对多个编写器进行了优化。
ii)如何设计get(key)和getStrict(key)操作?这是考虑它的正确方法吗?
您描述的是Concurrent集合现在如何工作。我不清楚getStrict的不同之处。
iii)如何设计我的地图以利用递增键和稀疏值范围?
如果你有普通的递增键,也许Ring Buffer是更好的选择。如果你有sparse values
所有你需要做的就是存储值。
iv)如何以最佳方式处理频繁删除?
根据您的操作,环形缓冲区对删除非常有效。要考虑的主要事情是拥有内存/对象回收策略。这将降低重新分配和垃圾收集的成本。
任何调整大小/重新散列都需要立即对读者线程可见,而不是最终可见。
如果值最终可以保持一致,我不明白为什么需要立即调整大小。