【问题标题】:What is the memory visibility of variables accessed in static singletons in Java?Java中静态单例中访问的变量的内存可见性是什么?
【发布时间】:2013-08-25 06:01:27
【问题描述】:

我在项目中经常看到这种类型的代码,其中应用程序需要一个全局数据持有者,因此他们使用任何线程都可以访问的静态单例。

public class GlobalData {

    // Data-related code. This could be anything; I've used a simple String.
    //
    private String someData;
    public String getData() { return someData; }
    public void setData(String data) { someData = data; }

    // Singleton code
    //
    private static GlobalData INSTANCE;
    private GlobalData() {}
    public synchronized GlobalData getInstance() { 
       if (INSTANCE == null) INSTANCE = new GlobalData();
       return INSTANCE; 
    }
}

我希望很容易看到发生了什么。可以随时在任何线程上调用GlobalData.getInstance().getData()。如果两个线程以不同的值调用 setData(),即使你不能保证哪一个“获胜”,我也不担心。

但线程安全不是我关心的问题。我担心的是内存可见性。每当Java中存在内存屏障时,缓存的内存就会在相应的线程之间同步。在通过同步、访问 volatile 变量等时会发生内存屏障。

想象以下按时间顺序发生的场景:

// Thread 1
GlobalData d = GlobalData.getInstance();
d.setData("one");

// Thread 2
GlobalData d = GlobalData.getInstance();
d.setData("two");

// Thread 1
String value = d.getData();

难道线程1中value的最后一个值还可以是"one"吗?原因是,线程 2 在调用 d.setData("two") 之后从未调用任何同步方法,所以从来没有内存屏障?请注意,这种情况下的内存屏障在每次调用 getInstance() 时都会发生,因为它是同步的。

【问题讨论】:

  • 您说One can call GlobalData.getInstance().getData() at any time on any thread. It's thread-safe so I'm not worried about that,但由于您描述的场景,它实际上并不是线程安全的。线程安全不仅仅是看到处于不一致状态的对象。这也与查看陈旧数据有关。
  • @yair 是的,但我真的试图检查内存可见性方面,而不是基于同步执行的不一致状态。

标签: java java-memory-model


【解决方案1】:

你说的很对。

不能保证写入一个Thread 将是可见的另一个。

要提供这种保证,您需要使用 volatile 关键字:

private volatile String someData;

顺便说一句,您可以利用 Java 类加载器来提供单例的线程安全惰性初始化,如文档 here 所述。这避免了synchronized 关键字,因此为您节省了一些锁定。

值得注意的是,当前公认的最佳实践是使用enum 在 Java 中存储单例数据。

【讨论】:

    【解决方案2】:

    正确,线程 1 可能仍将值视为“一”,因为没有发生内存同步事件,并且在线程 1 和线程 2 之间的关系之前也没有发生任何事件(参见 17.4.5 of the JLS 部分)。

    如果 someDatavolatile,那么线程 1 会将值视为“二”(假设线程 2 在线程 1 获取值之前完成)。

    最后,题外话,单例的实现有点不理想,因为它在每次访问时都会同步。通常最好使用枚举来实现单例,或者至少在静态初始化器中分配实例,这样getInstance 方法中就不需要调用构造函数。

    【讨论】:

      【解决方案3】:

      难道线程1中value的最后一个值还可以是“一”吗?

      是的。 Java 内存模型基于发生在 (hb) 之前的关系。在您的情况下,由于 synchronized 关键字,您只有 getInstance exit occur-before 随后的 getInstance entry

      所以如果我们以你的例子为例(假设线程交错是按这个顺序):

      // Thread 1
      GlobalData d = GlobalData.getInstance(); //S1
      d.setData("one");
      
      // Thread 2
      GlobalData d = GlobalData.getInstance(); //S2
      d.setData("two");
      
      // Thread 1
      String value = d.getData();
      

      你有 S1 hb S2。如果您在 S2 之后从 Thread2 调用 d.getData(),您会看到“one”。但是 d 的最后一次读取不能保证看到“二”。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-08-13
        • 2014-05-08
        • 1970-01-01
        • 2015-09-15
        • 1970-01-01
        • 2014-02-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多