我正在用 Java 编写一个不可变的 DOM 树,以简化多线程的访问。*

然而,它确实需要尽可能快地支持插入和更新。由于它是不可变的,如果我对树的第 N 层上的节点进行更改,我需要分配至少 N 个新节点才能返回新树。

我的问题是,预先分配节点而不是每次修改树时创建新节点会快得多吗?这将是相当容易做到的 - 保留一个包含数百个未使用节点的池,并在修改操作需要时从池中拉出一个而不是创建一个。当没有其他事情发生时,我可以补充节点池。(如果不明显的话,在这个应用程序中执行时间将比堆空间更宝贵)

这样做值得吗?还有其他加快速度的技巧吗?

或者,有人知道是否已经有了不可变的 DOM 库吗?我搜索过,但没有找到任何东西。

*笔记:对于那些不熟悉不变性概念的人来说,它基本上意味着在对对象进行任何更改它的操作时,该方法将返回包含更改的对象的副本,而不是更改后的对象。因此,如果另一个线程仍在读取该对象,它将继续愉快地在“旧”版本上操作,而不知道已进行更改,而不是严重崩溃。看 http://www.javapractices.com/topic/TopicAction.do?Id=29

有帮助吗?

解决方案

如今,对象创建速度非常快,并且对象池的概念有点过时了(至少在一般情况下是这样;连接池当然仍然有效)。

避免过早优化。在进行复制时需要时创建节点,然后看看速度是否会变得非常慢。如果是这样,那么请研究一些加快速度的技术。但除非您已经知道您所拥有的速度不够快,否则我不会介绍池化运行所需的所有复杂性。

其他提示

我不想给出一个非答案,但我认为回答这样的性能问题的唯一确定的方法可能是对这两种方法进行编码,对两者进行基准测试,并比较结果。

我不确定您是否可以避免显式同步某些方法以确保一切都是线程安全的。

在一种特定情况下,您需要同步一侧或另一侧,以使新创建的节点可供其他线程使用,否则您将面临 VM/CPU 重新排序字段写入超过共享节点引用写入的风险,从而暴露一方构造的对象。

尝试在更高的层面上思考。您有一个 IMMUTABLE 树(基本上是一组指向其子节点的节点)。您想在其中插入一个节点。那么,就没有退路了:你必须创建一棵新的整棵树。

如果您选择将树实现为一组指向子节点的节点,则必须沿着更改的节点到根的路径创建新节点。其他的与以前具有相同的值,并且通常是共享的。因此,您需要创建一个部分新树,这通常意味着(编辑节点的深度)父节点。

如果您可以应对不太直接的实现,那么您应该能够使用类似于中描述的技术来仅创建节点的一部分。 纯函数式数据结构 要么降低创建的平均成本,要么您可以使用半功能性方法绕过它(例如创建一个包装现有迭代器的迭代器,但返回新节点而不是旧节点,以及修复机制随着时间的推移,结构中会出现这样的补丁)。在这种情况下,XPath 风格的 api 可能比 DOM api 更好 - 它可能会让您将节点与树进一步解耦,并更智能地处理变异树。

我对你一开始想要做什么有点困惑。您希望所有节点都是不可变的并且希望将它们集中起来?这两个想法不是互相排斥的吗?当您从池中取出一个对象时,是否必须调用 setter 来连接子对象?

我认为使用不可变节点可能不会首先为您提供所需的线程安全性。如果 1 个线程正在迭代节点(搜索或其他内容),而另一个线程正在添加/删除节点,会发生什么情况?搜索结果会不会无效?我不确定您是否可以避免显式同步某些方法以确保一切都是线程安全的。

@不法程序员

当您将一个物体从游泳池中拉出时,您是否不得不调用一个固定器来将孩子链接起来?

每个节点对于包的内部不需要是不可变的,只需对于向外的接口是不可变的。 node.addChild() 将是一个具有公共可见性的不可变函数并返回一个 Document,而 node.addChildInternal() 将是一个具有包可见性的正常的、可变的函数。但由于它是包的内部,因此只能将其称为包的后代 addChild() 并且整个结构保证是线程安全的(假设我同步对对象池的访问)。你觉得这有什么缺陷吗...?如果是这样,请告诉我!

我认为使用不可变节点可能不会首先为您提供所需的线程安全性。如果 1 个线程正在迭代节点(搜索或其他内容),而另一个线程正在添加/删除节点,会发生什么情况?

整个树将是不可变的。假设我有 Thread1 和 Thread2,以及树 dom1。Thread1 在 dom1 上启动读操作,同时,Thread2 在 dom1 上启动写操作。然而,Thread2 所做的所有更改实际上都会对新对象 dom2 进行,而 dom1 将是不可变的。确实,Thread1 读取的值会过时(几微秒),但它不会因 IndexOutOfBounds 或 NullPointer 异常而崩溃,或者在读取正在写入的可变对象时发生类似情况。然后,Thread2 可以向 Thread1 触发包含 dom2 的事件,以便它可以再次进行读取并更新其结果(如果需要)。

编辑:澄清了

我认为@Outlaw 有道理。DOM 树的结构驻留在节点本身中,有一个节点指向其子节点。要修改树的结构,您必须修改节点,因此您不能将其合并,您必须创建一个新节点。

尝试在更高的层面上思考。您有一个 IMMUTABLE 树(基本上是一组指向其子节点的节点)。您想在其中插入一个节点。那么,就没有退路了:你必须创建一棵新的整棵树。

是的,不可变树是线程安全的,但它会影响性能。对象创建可能很快,但不会比不创建对象更快。:)

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top