我想要一个满足一些基本要求的对象列表。它需要支持快速随机访问,并且需要从多个线程安全使用。读取将占主导地位,理想情况下应该与正常NA
访问一样快,即无锁定。无需在中间插入元素,删除或更改索引处的值:唯一需要的突变是能够将新元素附加到列表的末尾。更具体地说,调用者将指定应该放置元素的索引,并且索引预期仅比列表的长度多几个,即列表是密集的。也没有必要进行迭代。
在Java中有什么支持这个吗?它可以在第三方库中。
如果不是,我想我会实现自己的课程。将有一个内部数组数组,每个数组都是最后一个数组的两倍。按索引查找将只做一些数学计算,以确定哪个数组具有正确的元素以及该数组中的索引是什么。除非超出可用空间,否则追加将类似,在这种情况下,将分配新数组。只有创建新数组才需要获取锁定。
这听起来像是一种明智的做法吗?
这听起来不像是一个特别新颖的数据结构。它有名字吗?
答案 0 :(得分:1)
使用Collections.synchronizedList(...)
包装的任何列表都符合您所声明的要求。
然而:
插入除列表末尾以外的任何地方都将是并发瓶颈。列表越长,它就越糟糕。
javadocs中有关于迭代的注意事项你应该阅读。
CopyOnWriteArrayList
是一种替代方法,但无论您在何处插入元素,写入时复制列表上的所有更新都是O(N)
。这是昂贵的,并且会有多个写入者并发瓶颈。可以忽略更新成本的论点仅适用于写入与读取的比率随时间降低的情况。如果比率随时间不变,则需要考虑(O(N)
)费用更新。
请注意,ArrayList的同步包装器将在列表末尾添加O(1)
查找和(摊销)O(1)
插入。不可否认,插入到列表中间的是O(N)
...但是没有我知道的列表结构比在随机位置插入更好O(logN)
。 (查找“可索引的跳过列表”。)
<强>更新强>
您评论道:
“我不需要随机插入,只需追加,除了追加的位置可能超出列表的末尾。例如,我可能有一个列表
[0,1,2]
,并希望插入{{ 1}}在索引4
,所以我的列表将是4
。“
如果这是对您的问题的正确描述,那么您所谈论的数据结构不是“列表”。当然,它与Java [0,1,2,null,4]
API不兼容。在List
上下文中,追加意味着在列表的当前最后一个元素之后立即添加元素;即位置== List
。
也许您应该寻找并发的稀疏数组类。这是一种可能性:
答案 1 :(得分:1)
读取将占主导地位,理想情况下应该与正常的ArrayList访问一样快,即没有锁定。
CopyOnWriteArrayList通常适用于这种情况,因为插入的成本将通过大量廉价的读取访问来分摊。
在附加条件下,只有通过预先调整阵列大小并保持一个单独的长度并在插入后原子地撞击它,可以进一步摊销它。
只有在您担心插入的峰值延迟时才需要其他方法。但这不是你提到的标准之一。
您还必须记住,您要求为您的用例(仅附加,无锁,O(1)访问等)定制数据结构,而JDK提供了通用的数据结构,可以进行一些权衡,以涵盖更多的用例。
有第三方库为有限的用例提供更专业的实现。
您描述的数据结构类型是一个spined缓冲区,并且在某些地方由JDK内部使用(例如以java.util.stream.SpinedBuffer<E>
的形式),但该实现不是线程安全的,因为它没有实现集合API。
它的javadocs状态:
一个或多个数组用于存储元素。使用多个 数组比使用的单个数组具有更好的性能特征 通过ArrayList,当需要增加列表的容量时 不需要复制元素。这通常是有益的 结果将被遍历的次数很少。
即。它主要用于一次写入,几次读取的场景,其中分配成本占主导地位。
在读取繁重的数据结构中,间接,额外数学运算和非顺序存储器访问的成本实际上可能超过偶尔复制/重新分配的成本。
答案 2 :(得分:1)
Java在java.util.concurrent. CopyOnWriteArrayList中有一个并发列表实现,它是ArrayList
的线程安全变体,其中所有的变异操作(添加,设置等)都是通过制作一个新的副本来实现的。底层数组。
来自doc:
这通常成本太高,但效率可能更高 遍历操作的数量远远超过突变,并且 当你不能或不想同步遍历时,它很有用 需要排除并发线程之间的干扰。 “快照” style iterator方法使用对数组状态的引用 迭代器的创建点。此数组永远不会更改 在迭代器的使用寿命期间,干扰是不可能的 保证迭代器不会抛出 ConcurrentModificationException的。迭代器不会反映出来 自迭代器以来添加,删除或更改列表 创建。对迭代器本身进行元素更改操作(删除, 不支持set和add)。抛出这些方法 UnsupportedOperationException异常。
允许使用所有元素,包括null。
根据您的要求:
读取将占据主导地位,理想情况下应该尽可能快 正常的ArrayList访问,即没有锁定。无需插入 中间的元素,删除或更改索引处的值: 只需要突变才能将新元素附加到 列表的末尾。
在末尾追加元素将导致底层数组的新副本(O(n)
)并且可能过于昂贵。我相信使用Collection.synchronizedList
可能是一个不错的选择,但这涉及锁定(阻止)。
同时检查this。