将一个 Java 对象引用替换为另一个被认为是线程安全的,还是我忽略了潜在的同步性问题?



我正在推出我自己的简单缓存解决方案,以改善冗长查找的用户体验。基本的概要是,我有一个处理所有accountId查找的类,还有一个名为AccountListCache的类,它有两个变量:一个是指示创建时间的时间戳,另一个是包含一大堆accountId对象的List对象。当调用查找时,该类将首先检查其缓存对象是否具有过去X分钟内的时间戳。如果是,它将提供缓存的结果,如果不是,它将刷新缓存并提供新列表。

我的计划是有一个计划任务,通过创建AccountListCache的新实例,填充其列表/时间戳,然后在查找类中重新分配变量,定期刷新缓存。我的老板提出了对线程安全的担忧,但我认为这应该已经是线程安全的了。

旧列表和新列表是包含在同一类的单独实例中的单独对象,而真正重要的更新调用只是更改引用在内存中指向的位置。这应该是一个有效的即时/原子操作,对吧?

如果它不是线程安全的,失败/冲突是什么样子的,我该如何解决它?

是的,由于一个线程正在写入数据以供其他线程读取,因此这里存在并发问题。

虽然写和读对象引用是原子的;撕裂";保证不会发生(与doublelong类型不同),除非使用内存屏障,否则无法保证写入对其他线程可见。

在这种情况下,我至少会将查找类中的AccountListCache字段设置为volatile。这将确保在将缓存分配给字段之前修改的缓存的任何状态对于读取该字段的任何线程都是可见的。


更新:

…我想了解它为什么失败以及如何失败,因为我怀疑在不久的将来我将面临更棘手的问题。

这些问题的权威答案是Java语言规范,§17.4首先,您应该理解"与";(§17.4.4)和";以前发生过"(§17.4.5)然后,您可以参考同步点列表,并在关系之前发生,以设计行为正确的并发交互。

在您的情况下,一个线程("编写器")需要填充一个列表,然后将其分配给另一个对象的字段,并让另一个线程读取该字段并访问该列表。使用§17.4.5中的先发生后发生关系,我们可以构建这个(部分)链(其中"hb(x,y)");表示x发生在y之前):

  • 如果x和y是同一线程的操作,并且x按程序顺序在y之前,则hb(x,y)。
    • x——缓存已初始化(写入程序)
    • y——对缓存的引用被写入字段(写入器)
  • 如果x和y是同一线程的操作,并且x按程序顺序位于y之前,则hb(x,y)。
    • x——从字段(读取器)读取对缓存的引用
    • y——访问缓存的内容(读取器)

您可能认为这已经足够了。但请注意关键词";相同线程的">这允许操作重新排序,如果它们不改变该线程的世界视图在您的情况下,可能会在将新项添加到新缓存实例之前分配引用缓存的字段。这种更改在线程内部是不可见的,但其他线程可以在填充缓存之前开始读取缓存。

当使缓存字段volatile时;与";写入和随后读取之间的关系;[a] 对易失性变量v(§8.3.1.4)的写入与任何线程对v的所有后续读取同步">

通过使字段不稳定,我们可以在链中添加更多步骤:

  • 如果一个动作x与后面的动作y同步,那么我们也有hb(x,y)。
    • x——缓存被分配给volatile字段(写入程序)
    • y——从易失性字段读取缓存(读取器)
  • 如果hb(x,y)和hb(y,z),那么hb(x,z)。
    • x——缓存已初始化(写入程序)
    • y—已分配缓存(写入程序)
    • z—读取缓存(读取器)

因此,我们已经证明了缓存在被读取器读取之前由写入器初始化。如果没有与关系同步,则会在关系丢失之前发生这种情况。

erickson的回答是正确的。

作为volatile的替代方案,我更喜欢使用AtomicReference,原因有两个:

  • 许多程序员并不完全理解volatile,无论是一般的还是具体的,因为它的含义在Java内存模型修订后发生了变化
  • AtomicReference确实向人类读者发出了一个警告:我们有一个并发问题需要管理

原子引用的示例。注意final,以保证我们只有一个AtomicReference实例。

final AtomicReference < AccountListCache > cacheRef = new AtomicReference<>( … ) ;

我会使用一个记录来保存您当前的缓存值,一个带有列表的时间戳。记录的成员字段引用是不可变的(有getter但没有setter)。

record AccountListCache ( Instant whenFetched , List< Account > ) {}

使用List.copyOf获取不可修改的列表。

您可以将构造函数添加到记录中以进行数据验证。你可以检查一下过去的瞬间是否在现在之前。你可以确保这个列表不是空的(如果不能忍受的话)。您可以通过List.copyOf放置传递的列表,以确保它不可修改(如果copyOf已经不可修改,则它是一个虚拟的no-op)。

使用ScheduledExecutorServiceAtomicReference中的记录引用重复替换为对AccountListCache记录类的新实例的引用。

任何使用缓存的代码都可以验证它的新鲜程度

Duration ageOfCache = 
Duration.between ( 
cacheRef.get().whenFetched() , 
Instant.now() 
) 
;

或者把这种方法记录在案。

record AccountListCache ( Instant whenFetched , List< Account > ) 
{
Duration age () { return Duration.between ( this.whenFetched , Instant.now() ) } 
}

用法:

Duration ageOfCache = cacheRef.get().age() ;

相关内容

  • 没有找到相关文章

最新更新