在Java中,同时更改对HashMap读取的引用是否安全

2022-09-03 04:47:38

我希望这不是一个太愚蠢的问题...

我的项目中有类似于以下内容的代码:

public class ConfigStore {

    public static class Config {

        public final String setting1;
        public final String setting2;
        public final String setting3;

        public Config(String setting1, String setting2, String setting3) {
            this.setting1 = setting1;
            this.setting2 = setting2;
            this.setting3 = setting3;
        }

    }

    private volatile HashMap<String, Config> store = new HashMap<String, Config>();

    public void swapConfigs(HashMap<String, Config> newConfigs) {
        this.store = newConfigs;
    }

    public Config getConfig(String name) {
        return this.store.get(name);
    }

}

在处理请求时,每个线程将使用 getConfig() 函数从存储区请求一个配置。但是,定期(最有可能每隔几天)使用 swapConfigs() 函数更新并换出配置。调用 swapConfigs() 的代码不会保留对它传入的 Map 的引用,因为它只是解析配置文件的结果。

  • 在这种情况下,存储实例变量上是否仍需要关键字?volatile
  • 关键字是否会引入任何我应该注意或可以避免的潜在性能瓶颈,因为读取速率大大超过写入速率?volatile

非常感谢,


答案 1

由于更改引用是一个原子操作,因此即使您删除 ,您也不会最终看到一个线程修改引用,而另一个线程看到垃圾引用。但是,对于某些线程,新映射可能不会立即可见,因此可能会无限期(或永久)从旧映射读取配置。所以保持.volatilevolatile

更新

正如@BeeOnRope在下面的评论中指出的那样,有一个更充分的理由使用:volatile

“非易失性写入 [...]不要在写入和看到写入值的后续读取之间建立“发生之前”关系。这意味着线程可以看到通过实例变量发布的新映射,但此新映射尚未完全构造。这不是直观的,但这是记忆模型的结果,它发生在真实的单词中。要安全地发布对象,必须将其写入 到 ,或使用一些其他技术。volatile

由于您很少更改该值,因此我认为不会导致任何明显的性能差异。但无论如何,正确的行为胜过表现。volatile


答案 2

不,如果没有易失性,这不是线程安全的,即使除了看到过时值的问题。即使没有对映射本身进行写入,并且引用赋值是原子的,新的引用分配也尚未安全发布Map<>

为了安全地发布对象,必须使用某种机制将其传达给其他线程,该机制要么在对象构造,引用发布和引用读取之间建立先发生关系,要么必须使用一些更窄的方法,这些方法保证发布是安全的:

  • 从静态初始值设定项初始化对象引用。
  • 将对它的引用存储到最终字段中。

这两种特定于发布的方式都不适用于您,因此您需要不稳定来建立发生之前。

以下是此推理的较长版本,包括指向JLS的链接以及一些如果您不安全发布可能发生的事情的示例。

有关安全发布的更多详细信息,请参阅JCIP(强烈推荐)或此处


推荐