我正在推出我自己的简单缓存解决方案来改进冗长查找的用户体验。基本概述是我有一个处理所有 accountId 查找的类,另一个名为 AccountListCache 的类有两个变量:一个指示它何时创建的时间戳,以及一个包含一大堆 accountId 对象的 List 对象。调用查找时,该类将首先检查其缓存对象是否具有过去 X 分钟内的时间戳。如果是,它会提供缓存的结果,如果不是,它会刷新缓存并提供新列表。
我的计划是通过创建 AccountListCache 的新实例、填充其列表/时间戳,然后在查找类中重新分配变量来创建定期刷新缓存的计划任务。我的老板提出了对线程安全的担忧,但我认为这应该已经是线程安全的了。
旧列表和新列表是包含在同一类的不同实例中的不同对象,重要的实际更新调用只是更改引用在内存中指向的位置。这应该是一个有效的瞬时/原子操作,对吧?
如果它不是线程安全的,失败/冲突是什么样的,我该如何解决?
最佳答案
是的,因为一个线程正在写入数据以供其他线程读取,所以这里存在并发问题。
虽然写入和读取对象引用是原子的,但在保证不会发生“撕裂”的意义上(不像 double
或 long
类型),没有保证除非使用内存屏障,否则写入将永远对其他线程可见。
在这种情况下,至少,我会将查找类中的 AccountListCache
字段设为 volatile
。这将确保缓存在分配给该字段之前修改的任何状态对读取该字段的任何线程都是可见的。
更新:
… I'd like to understand both why it fails and how, because I suspect I'm about to face trickier problems in the [near] future.
这些问题的权威答案是Java Language Specification, §17.4首先,您应该理解“同步于”(§17.4.4)和“先于发生”的概念。 (§17.4.5)然后您可以引用同步点列表和先行关系来设计行为正确的并发交互。
在您的情况下,一个线程(“作者”)需要填充一个列表,然后将其分配给另一个对象的字段,并让另一个线程(“读者”)读取该字段并访问列表。使用 §17.4.5 中的 happens-before 关系,我们可以构建这个(部分)链(其中“hb(x, y)”表示 x happens-before y):
- 如果 x 和 y 是同一线程的 Action ,并且 x 在程序顺序中出现在 y 之前,则 hb(x, y)。
- x——缓存已初始化(writer)
- y——对缓存的引用被写入一个字段(writer)
- 如果 x 和 y 是同一线程的 Action ,并且 x 在程序顺序中出现在 y 之前,则 hb(x, y)。
- x——从字段(reader)中读取对缓存的引用
- y——缓存内容被访问(reader)
您可能认为这就足够了。但请注意关键词“同一线程”。如果它们不改变那个线程的世界 View ,这允许重新排序操作。在你的在这种情况下,可能会在将新项目添加到新缓存实例之前分配引用缓存的字段。该更改在线程内部不可见,但其他线程可以在填充缓存之前开始读取缓存。
当您使缓存字段volatile
时,您在写入和后续读取之间引入了“同步”关系,因为“[a] 写入一个易失变量 v(§8.3.1.4 ) 与任何线程对 v 的所有后续读取同步。”
通过使字段可变,我们可以向链中添加更多步骤:
- 如果 Action x 与后续 Action y 同步,那么我们也有 hb(x, y)。
- x——缓存分配给volatile字段(writer)
- y——从volatile字段(reader)读取缓存
- 如果 hb(x, y) 和 hb(y, z),则 hb(x, z)。
- x——缓存已初始化(writer)
- y——缓存已分配(写入者)
- z——缓存被读取(reader)
因此,我们证明了缓存是在读取器读取之前由写入器初始化的。如果没有 synchronizes-with 关系,happens-before 关系就会丢失。
关于java - 将一个 Java 对象引用替换为另一个被认为是线程安全的对象引用,还是我忽略了潜在的同步性问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72636248/