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

Mid*_*int 14 java concurrency multithreading volatile

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

我的代码中的代码类似于以下代码:

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);
    }

}
Run Code Online (Sandbox Code Playgroud)

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

  • 在这种情况下,volatile商店实例变量仍然需要关键字吗?
  • volatile关键字带来任何潜在的性能瓶颈,我应该知道的或可避免因为读取的速度大大超过写入的速度?

非常感谢,

Pét*_*rök 11

由于更改引用是一个原子操作,因此即使丢弃,也不会有一个线程修改引用,另一个线程看到垃圾引用volatile.但是,新映射可能无法立即显示某些线程,因此可能会无限期地(或永久地)从旧映射中继续读取配置.所以保持volatile.

更新

正如@BeeOnRope在下面的评论中指出的那样,有更强的理由可以使用volatile:

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

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

  • 这是不正确的.你需要挥发性的.虽然非易失性写入是原子的,但它们不会在写入和后续读取之间的关系之前建立发生的事件.这意味着线程可以看到通过实例变量发布的新地图,但是这个新地图还没有完全构建.这不是直观的,但它是内存模型的结果,它发生在真实的单词中.对于要安全发布的对象,必须将其写入易失性或使用少数其他技术. (4认同)

Bee*_*ope 11

不,这不是没有易失性的线程安全,即使除了看到陈旧值的问题.即使没有对地图本身的写入,并且引用分配是原子的,新的Map<>也没有安全地发布.

对于要安全发布的对象,必须使用某种机制将其传递给其他线程,该机制要么在对象构造,参考发布和参考读取之间建立先发生关系,要么必须使用少数较窄的方法.保证安全发布:

  • 从静态初始化程序初始化对象引用.
  • 将对它的引用存储到最终字段中.

这两种出版物特定方式都不适用于您,因此您需要使用volatile来建立之前发生的事情.

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

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


Tom*_*icz 9

你的代码很好.您需要volatile,否则您的代码将是100%线程安全的(更新引用是原子的),但是更改可能对所有线程都不可见.这意味着一些线程仍将看到旧值store.

volatile在你的例子中,这是必须的.你可能会考虑AtomicReference,但在你的情况下它不会给你更多.

您无法交换性能的正确性,因此您的第二个问题并不真正有效.它会产生一些性能影响,但可能只是在更新期间,这种情况很少发生,正如您所说.基本上JVM将通过"刷新"来确保所有线程都可以看到更改,但之后它将可以作为任何其他本地变量访问(直到下次更新).

顺便说一下,我喜欢Config类是不可变的,请考虑不可变的Map实现以防万一.

  • +1:Map是否在运行时使用Collections.unmodifiableMap()显式变为不可变,您应该在设置后更改Map的内容.如果你需要能够这样做,请使用ConcurrentHashMap并使其最终,即使用putAll和keySet().retainAll()来更新ConcurrentHashMap (2认同)
  • 正确性需要volatile,而不仅仅是可见性 (2认同)
  • 我同意@BeeOnRope:**代码不是没有volatile的线程安全** - 不仅一个线程可以看到陈旧的地图引用,而且它可以看到一个指向不一致地图的最新引用(即不正确建造). (2认同)