我总是听到人们说,在使用多个线程时管理不可变对象更容易,因为当一个线程访问不可变对象时,不必担心另一个线程正在更改它。
那么,如果我有一个公司所有员工的不可变列表并且雇用了一名新员工,会发生什么情况?在这种情况下,必须复制不可变列表,并且它的新副本必须包含另一个员工对象。然后,对员工列表的引用应定向到新列表。
当这种情况发生时,列表本身不会改变,但对此列表的引用会改变,因此代码“看到”不同的数据。
如果是这样,我不明白为什么不可变对象使我们在使用多线程时生活更轻松。我缺少什么?
可变数据并发更新的主要问题是,线程可能会感知来自不同版本的变量值,即,在单个更新时,新旧值的混合,形成不一致的状态,违反了这些变量的不变量。
例如,参见 Java 的ArrayList
。它有一个int
保存当前大小的字段和对数组的引用,该数组的元素是对所包含对象的引用。这些变量的值必须满足某些不变量,例如如果大小非零,则数组引用永远不会null
并且数组长度总是大于或等于大小。当看到这些变量的不同更新值时,这些不变量不再成立,因此线程可能会看到从未以这种形式存在的列表内容,或者因虚假异常而失败,报告应该不可能的非法状态(例如NullPointerException
or ArrayIndexOutOfBoundeException
).
请注意,线程安全或并发数据结构仅解决有关数据结构内部的问题,因此操作不会再因虚假异常而失败(关于集合的状态,我们还没有讨论所包含元素的状态),但是操作迭代这些集合或以任何形式查看多个包含的元素,仍然可能会观察到有关包含的元素的不一致状态。这也适用于先检查然后执行的反模式,其中应用程序首先检查条件(例如使用contains
),然后对其进行操作(例如获取、添加或删除元素),而条件可能会在中间发生变化。
相反,处理不可变数据结构的线程可能会处理其过时的版本,但属于该结构的所有变量彼此一致,反映相同的版本。执行更新时,您不需要考虑排除其他线程,这根本没有必要,因为其他线程看不到新的数据结构。发布新版本的整个任务简化为发布对数据结构新版本的根引用的任务。如果您无法阻止其他线程处理旧版本,则可能发生的最糟糕的情况是您之后可能必须使用新数据重复操作,换句话说,在最坏的情况下,这只是一个性能问题。
这对于具有垃圾收集功能的编程语言来说很顺利,因为它们允许新的数据结构引用旧对象,只需替换更改的对象(及其父对象),而无需担心哪些对象仍在使用中,哪些没有使用。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)