【问题标题】:Java forcing volatile accessJava 强制可变访问
【发布时间】:2018-01-15 19:21:19
【问题描述】:

考虑这样的情况。

有两个线程和一个共享资源(如 HashMap)。一个线程创建了 HashMap 并使用一些键值对对其进行了初始化,并且在初始化共享资源后,它将永远不会再被修改。

现在,第二个线程是在共享资源初始化并想要使用该资源之后严格创建的。在这一点上,我想保证第二个线程将使用正确版本的共享资源。我认为第一个线程可能在创建第二个线程之前没有将更改刷新到主内存,因此第二个线程会将共享资源的旧值放入它的缓存中。

此分析是否正确,以及如何在初始化共享资源后手动强制刷新到 Java 中的主内存,因为在这种我不想要或不需要 volatilesynchronized 的特殊情况下。

【问题讨论】:

  • @Robert 这根本不是真的。
  • 我对您的要求有些困惑。如果您想使用“同步”使某些内容同步,请使用 ConcurrentHashmap。如果你想确保一个线程首先工作,然后另一个工作,你可以设置一个可重入锁并让第二个线程等到哈希图被释放。或者您可以用另一个线程启动一个线程,以绝对保证第二个线程将使用正确的版本。

标签: java multithreading volatile


【解决方案1】:

The documentation 说:

在线程上启动的调用发生在已启动线程中的任何操作之前。

所以,如果您的代码符合您的描述,那就是安全的。

【讨论】:

  • ...假设“第二个线程严格在之后创建”确实意味着根据 Java 内存模型进行排序,例如如果地图完全初始化后第一个线程调用start()。举个反例,依靠Thread.sleep 来假设按时间排序是不正确的。
【解决方案2】:

如果您将 HashMap 声明并初始化为静态字段,它将由 Java 类加载器以线程安全的方式初始化。

【讨论】:

  • 共享对象的初始化需要在运行时进行,由第一个线程完成。
【解决方案3】:

如果地图初始化发生在第二个线程开始之前,那么一切都是正确的。为了简化分析并使事情变得简单,您可以将初始化的映射转换为一些不可变的映射实现并将其显式传递给创建的线程。这样,您根本不需要使用共享变量。

【讨论】:

    【解决方案4】:

    这个分析是否正确,以及如何在初始化共享资源后手动强制刷新到 Java 中的主内存,就像在我不想要或不需要 volatile 或同步的这种特殊情况下一样。

    不可能不需要volatilesynchronized。您必须在线程之间使用某种形式的内存同步,否则无法正常工作。

    您可以使用 Andrei 提到的 static 初始化程序 (*) 或 final,这两者都意味着内存屏障。但是你必须使用一些东西。

    您可能需要同步地图 (Collections.synchronizedMap()) 或 CurrentHashMap,但您仍需要使用 volatilesynchronizedfinalstatic 来保护字段本身。

    参考Java Concurrency in Practice by Brian Geotz,还有这个 related question on Stack Overflow(请注意,OP 把书名弄错了)。

    (* 整个静态初始化器有点复杂,你应该阅读 Goetz 先生的书,但我会尝试简要描述一下:static 字段是类初始化的一部分。每个静态字段或静态初始化块由一个线程写入或执行(可能是调用new或第一次访问类对象的线程,也可能是不同的线程)。当写入所有static字段的过程为第一次完成时,JVM 会插入一个内存屏障,以便按照规范的要求,类对象及其所有静态字段对系统中的所有线程都是可见的。

    您不会在每次字段写入时遇到内存屏障,例如 volatile。类加载试图提高效率,并且只在初始化的最后插入一个屏障。因此,您应该只将static 初始化器用于它们应该用于的用途:第一次填充字段,并且不要尝试在静态初始化器块中编写整个程序。它效率不高,而且您对线程安全的选择实际上更加有限。

    然而,作为类初始化一部分的内存屏障可以使用,这就是为什么,Andrei Amarfii 说,在 Java 中使用静态初始化器的模式用于确保对象的可见性。 Brian Goetz 将其称为他的四种“安全发布”模式之一,这一点非常重要。)

    【讨论】:

    • @Holger 是的。请阅读 Java 并发实践。 Brian Goetz 解释了它是如何工作的。基本上,static 表示该字段由类加载器初始化,并且在类加载结束时必须有一个内存屏障,因为系统中的任何线程都可以访问类对象,并且JVM 必须确保对其的所有初始写入都是可见的。另请阅读我链接到的关于安全出版物的问题;明确提到了静态初始化。
    • 如果你在类初始化器中初始化一个static字段,你会得到一个内存屏障,但这是初始化器的一个属性,而不是因为该字段是@987654340 @,因为一个字段是static 的事实并不意味着它将在类初始化程序中初始化,或者它以后永远不会被修改。按照您编写的方式,读者会认为随时修改static 字段是安全的,但事实并非如此。据我记得,本书的所有示例都使用了static final,它强制正确使用,但与您的“finalstatic”不匹配。
    • 我将更新我的答案以反映内存屏障仅适用于静态初始化程序,这很公平。对于其余部分,您确实会在所有静态字段上(最初)获得内存屏障,初始化它们的唯一方法是在类加载期间,即使它只是默认值并且没有显式初始化程序。 'final' 即使在实例字段上也会产生内存屏障(这就是你获得不可变对象的方式),所以那里的'或'是完全正确的。两者都行,两者都只是一个常见的习语。
    猜你喜欢
    • 1970-01-01
    • 2011-11-28
    • 2019-05-30
    • 2018-05-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-26
    相关资源
    最近更新 更多