【问题标题】:Efficient concurrent tree高效并发树
【发布时间】:2012-06-25 12:54:48
【问题描述】:

我正在寻找一种有效的方法来实现并发树结构。如果这有帮助,请假设我的读取访问权限比对结构的更改要多得多。

树应该支持这些操作:

  • 添加和删除节点
  • 每次插入新节点时对分支进行排序
  • 遍历所有节点(没有 ConcurrentModificationException)
  • 按路径查找元素

【问题讨论】:

    标签: java concurrency tree


    【解决方案1】:

    查看:Concurrent-Trees 上的 Google 代码,了解一种无需锁定即可修改树状结构的方法。

    该项目为 Java 提供并发基数和后缀树。它们支持并发读取和写入,并且读取是无锁的。它通过以原子方式将补丁应用于树来工作。虽然这些类型的树可能不是您想要的,但TreeDesign 中描述的使用“修补”的方法对于任何类型的树状结构都很有用。

    这些树旨在用于以高并发读取为主的用例,其中(例如)后台线程可能正在从树中插入或删除条目,而许多前台线程将继续遍历它而不受修改的阻碍。

    【讨论】:

    • 这很有趣,但在我的用例中没用。我的树是真正的树(父子结构),而不是键值映射。
    • 我会避免使用“无用”之类的词。这个答案是自愿帮助您的。如果您正在寻找以读取为主的并发树,那么无锁算法可能是一个不错的选择。基数树可能不是您正在寻找的,但正如我所提到的,我链接到的原子修补方法可以应用于任何类型的树以进行无锁读取,即使您打算编写自己的自定义树。顺便说一句,基数树显然是树,具有父子结构。
    • 新的措辞看起来不错。我还删除了关于文档的最后一段,因为我已将剩余文档添加到网站。
    • 这是我们最终使用的解决方案。主要缺点:内存消耗和更改操作非常昂贵,因为您每次都需要复制树。但是优点远远超过他们:简单的代码结构,没有死锁,没有读锁,很容易将多个树更改合并到一个操作中(所以我只需要一个副本),实现不泄漏,保证是线程安全的,没有极端情况。
    • 根据树的类型,可能不需要为每次修改复制整个(子)树。一些树(例如基数树)具有参考局部性(LOR),因此中间分支的修改只需为补丁分配几个节点,并且被替换节点的后代通常可以直接附加到未修改的补丁.在没有 LOR 的树中,写入肯定会更昂贵。由于您的树可以由客户修改,请尝试很好地要求他们观察参考位置!我很高兴这对您有所帮助,祝您的项目好运。
    【解决方案2】:

    最接近您可能需要的 Java 结构是 ConcurrentSkipListSet(或者可能是 ConcurrentSkipListMap)。


    如果您需要更自定义的方法,如果您有分层读写锁,则可以实现自定义树结构。 这是关于如何实现可重入读写锁的类似问题的答案: https://stackoverflow.com/a/6154873/272388

    【讨论】:

    • CSLM 并不是很接近。它是一个排序的地图,与树形结构关系不大。所以它只与TreeMap 共享一个共同的属性,因为它是一个排序的地图——这在这里不相关。
    • 这就是为什么会有第二部分的答案,因为我不知道 Java 中包含任何支持所有这些操作的树结构。此外,对于一些类似的情况,CSLM 可能就足够了。
    • Kru 仍然正确:它是官方运行时中唯一与我的问题最接近的数据结构。我可以使用嵌套的 ConcurrentSkipListMaps + 锁来构建树。
    • 有什么理由必须是树吗?嗯,并发(无锁)树形图是一个研究课题。是的,我们有 COW B-Tree,但它们很疯狂。
    • @AaronDigulla ConcurrentSkipListSet 不是实现单个节点的子容器的更好工具吗?
    【解决方案3】:

    您可以在结构中使用读写器锁,这样多个线程可以同时读取,但一次只有一个线程可以修改它。 如果某个线程试图修改结构,则在所有读者都完成阅读之前它无法修改。 如果一个线程想要读取它,那么只有在作者尚未工作时才能读取它,或者它正在做一些修改。 也许看看这个会有所帮助:

    http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html

    【讨论】:

      【解决方案4】:

      读/写锁的基本思路

      对于所有写入方法,请使用以下成语:

      someWriteMethod(){
        writeLock.lock();
        // logic
        writeLock.unlock();
      }
      

      对于所有读取方法使用类似的代码:

      someReadMethod(){
        readLock.lock();
        // logic
        readLock.unlock();
      }
      
      • 如果至少有一种方法执行写入,则没有人可以获得读或写锁。
      • 如果至少有一种方法执行读取,则任意数量的线程都可以获得读锁。
      • 如果至少有一种方法执行读取,则没有人可以获得写入锁。

      注意,如果您的代码(替换上面的逻辑注释)可能引发异常,请确保在 finally 部分的方法退出之前释放锁。

      【讨论】:

      • 读写锁有一个缺点:你不能将读锁“升级”为写锁,所以你需要提前知道你的操作是否会修改树。所以这需要两个迭代器,其中一个可能会锁定太多。
      • 我假设使用树你事先知道哪些操作会修改你的树,不是吗?
      • 当我向树请求迭代器时,创建迭代器的方法不知道我是否会调用 remove()。
      • 你的树很大吗?退回副本怎么样?
      • 我想知道自己。我的树通常不大,但没有人为限制它们,因此客户可以将它们制作成他们想要的大:-/
      猜你喜欢
      • 1970-01-01
      • 2016-02-15
      • 1970-01-01
      • 1970-01-01
      • 2012-03-05
      • 1970-01-01
      • 1970-01-01
      • 2015-05-05
      • 1970-01-01
      相关资源
      最近更新 更多