java - 如何避免在单例模式中使用 volatile 的性能开销?
问题描述
说单例模式的代码:
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 主要做两件事 - 确保原子性(多个线程不能同时进入同步块)并确保一旦创建,所有线程都会看到创建的实例,称为可见性。同时,它确保了同步块将进入一次,之后的所有线程都不需要这样做。
您可以通过以下方式轻松完成:
private Singleton instance;
public Singleton get() {
synchronized (this) {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
但是现在每个需要它instance
的线程都必须竞争锁并且必须进入那个同步块。
有些人认为:“嘿,我可以解决这个问题!” 并写入(因此只进入同步块一次):
private Singleton instance; // no volatile
public Singleton get() {
if (instance == null) {
synchronized (this) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
就这么简单——那就是坏了。这并不容易解释。
它被破坏了,因为有两个独立的读取;
instance
JMM 允许对这些进行重新排序;因此看不到空值是完全有效的;if (instance == null)
whilereturn instance;
看到并返回 anull
。是的,这是违反直觉的,但完全有效且可证明(我可以jcstress
在 15 分钟内编写一个测试来证明这一点)。第二点有点棘手。假设您的单身人士有一个需要设置的字段。
看这个例子:
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
不是空值,但在执行时instance.getSome()
会看到空值。正确的方法是(加上创建实例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
只要您的代码是正确的,您就不应该关心读取的内容;所以不要担心“从主内存读取”(或者最好不要在没有部分理解的情况下使用这个短语)。
推荐阅读
- ios - UIScrollView 中的多个 UITextField 在一定计数后不可点击
- sql - Oracle PL/SQL:SYSDATE 与“DD-MMM-YY”有何不同?
- php - php 7 中的 mysql 支持
- html - 如何防止我的动画结尾突然“卡入”?
- python - 数据框滚动大于 0 计数
- java - 从上述棉花糖设备上的应用程序中清除所有最近的应用程序
- java - 获取异常是 java.net.SocketException: Connection reset
- elasticsearch - 按“是数组中的项”对结果进行排序
- intellij-idea - sbt.compiler.EvalException:表达式中的类型错误
- python - 表数据问题