说出单例模式的代码:
class Singleton
{
private volatile static Singleton obj;
private Singleton() {}
public static Singleton getInstance()
{
if (obj == null)
{
synchronized (Singleton.class)
{
if (obj==null)
obj = new Singleton();
}
}
return obj;
}
}
上面代码中的obj被标记为Volatile,这意味着每当代码中使用obj时,它总是从主内存中获取而不是使用缓存值。因此,每当需要执行 if(obj==null)
时,它都会从主内存中获取 obj,尽管它的值是在上一次运行时设置的。这是使用 volatile 关键字的性能开销。我们如何避免它?
最佳答案
您严重误解了 volatile
的作用,但公平地说,互联网和 stackoverflow 包括关于此的错误或不完整的答案。我也承认我认为我对它有很好的把握,但有时不得不重新阅读一些东西。
您在那里展示的内容被称为“双重检查锁定”成语,它是创建单例的完全有效的用例。问题是你是否真的需要它(另一个答案显示了一种更简单的方法,或者如果你愿意,你也可以阅读“枚举单例模式”)。有点滑稽的是,有多少人知道这个习语需要 volatile
,但不能真正告诉为什么需要它。
DCL 主要做了两件事——确保原子性(多个线程不能同时进入同步块(synchronized block))并确保一旦创建,所有线程都会看到创建的实例,称为可见度。同时,它确保同步块(synchronized block)将被单次进入,之后的所有线程都不需要这样做。
您可以通过以下方式轻松完成:
private Singleton instance;
public Singleton get() {
synchronized (this) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
但是现在每个需要那个实例
的线程都必须竞争锁并且必须进入那个同步块(synchronized block)。
有些人认为:“嘿,我可以解决这个问题!”并写入(因此进入同步块(synchronized block)仅一次):
private Singleton instance; // no volatile
public Singleton get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
就这么简单 - 这就是损坏。而这并不容易解释。
它坏了,因为
实例
有两次独立读取; JMM 允许重新排序;因此 完全有效if (instance == null)
没有看到 null;而return instance;
看到并返回一个null
。是的,这是违反直觉的,但完全有效且可证明(我可以在 15 分钟内编写一个jcstress
测试来证明这一点)。第二点有点棘手。假设你的单例有一个你需要设置的字段。
看这个例子:
static class Singleton {
private Object some;
public Object getSome() {
return some;
}
public void setSome(Object some) {
this.some = some;
}
}
然后您编写这样的代码来提供该单例:
private Singleton instance;
public Singleton get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new Singleton();
instance.setSome(new Object());
}
}
}
return instance;
}
由于写入到volatile
(instance = new Singleton();
)发生在设置你需要的字段 instance.setSome(new Object());
;某些读取此实例的线程可能会看到 instance
不为 null,但是在执行 instance.getSome()
时会看到 null。正确的做法是(加上使实例 volatile
):
public Singleton get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
Singleton copy = new Singleton();
copy.setSome(new Object());
instance = copy;
}
}
}
return instance;
}
因此 volatile 是安全发布所必需的;这样所有线程都可以“安全地”看到已发布的引用 - 它的所有字段都已初始化。还有一些其他方法可以安全地发布引用,例如在构造函数中设置 final
等。
事实:读比写便宜;只要您的代码是正确的,您就不必关心 volatile
读取的内容;所以不要担心“从主内存读取”(或者最好不要在没有部分理解的情况下使用这个短语)。
关于java - 如何避免在单例模式中使用 volatile 的性能开销?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56964868/