我在项目中经常看到这种类型的代码,其中应用程序需要全局数据持有者,因此他们使用任何线程都可以访问的静态单例。
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()
时都会发生内存屏障,因为它已同步。
答案 0 :(得分:2)
你是完全正确的。
无法保证一个Thread
中的写入可见另一个。
要提供此保证,您需要使用volatile
关键字:
private volatile String someData;
顺便提一下,您可以利用Java类加载器来提供单例的线程安全延迟初始化,如文档here所示。这样可以避免synchronized
关键字,从而为您节省一些锁定。
值得注意的是,目前公认的最佳做法是使用enum
在Java中存储单例数据。
答案 1 :(得分:2)
正确,线程1可能仍然将值视为"一个"因为没有发生内存同步事件,所以在线程1和线程2之间的关系之前没有发生(参见17.4.5 of the JLS部分)。
如果someData
为volatile
,则广告1会将值视为"两个" (假设线程2在线程1获取值之前完成)。
最后,在主题之外,单例的实现略微不理想,因为它在每次访问时都是同步的。通常最好使用枚举来实现单例,或者至少在静态初始化程序中分配实例,因此getInstance
方法不需要调用构造函数。
答案 2 :(得分:1)
线程1中的最后一个值是否仍然可以是“1”?
是的。 java内存模型基于(hb)关系之前发生的事件。在您的情况下,由于synchronized关键字,您只有 getInstance
退出 - 在后续getInstance
条目之前。
因此,如果我们采用您的示例(假设线程交错是按此顺序):
// 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()
,你会看到“一个”。但最后一次阅读d并不能保证看到“两个”。