这段代码是否解决了Java中的双重检查锁定问题?

5
这段代码是否解决了Java中的双重检查锁问题?
public class DBAccessService() {
    private static DBAccessService INSTANCE;  

    private DBAccessService() {}

    public static DBAccessService getInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        return createInstance();
    }

    private static synchronized DBAccessService createInstance() {
        if (INSTANCE != null) {
            return INSTANCE;
        }
        DBAccessService instance = new DBAccessService();
        INSTANCE = instance;

        return INSTANCE;
    }
}

需要注意以下两个方面:

  1. getInstance()方法没有同步,因此在初始化INSTANCE后,不需要同步的成本。
  2. createInstance()方法是同步的。

所以,问题是:这段代码有任何问题吗?它是否合法并且始终线程安全?


2
很好的问题!我只想提醒您要小心任何未引用描述Java内存模型的JVM规范的答案 - 这太容易让人误解了。 - Nick Fortescue
@Ganeshkumar:erf,这是破损的DCL,但至少它试图保持正确!如果完全没有同步,您可能会实例化任意数量的新DBAccessService! - SyntaxT3rr0r
5个回答

8
您需要将INSTANCE声明为volatile才能使其正常工作:
private static volatile DBAccessService INSTANCE;

请注意,它只适用于Java 5及更高版本。请参见“双检锁定是错误的”声明


4

为了解决这个特定问题,Java并发实践(由基本上编写了java.util.concurrent库的团队撰写)建议使用懒加载Holder类惯用语法(我的副本第348页,清单16.6,而不是16.7)。

@ThreadSafe
public class DBAccessServiceFactory {
  private static class ResourceHolder {
    public static DBAccessService INSTANCE = new DBAccessService();
  }
  public static DBAccessService getResource() {
    return ResourceHolder.INSTANCE;
  }
}

这种写法一直是合法且线程安全的。我不是专家,所以不能说这比你的代码更好。然而,鉴于它是Doug Lea和Joshua Bloch推荐的模式,我会始终使用它而不是我们发明的代码,因为犯错误太容易了(正如对这个问题的错误答案数量所证明的那样)。
关于volatile问题,他们说:
在JMM(Java 5.0及更高版本)中的后续更改使得DCL可以工作,如果将resource设置为volatile...但是懒汉式初始化占位符模式提供了相同的好处,并且更容易理解。

很好的回答,Nick。我读过这本书,但可能没有准备好去阅读它。所以,我总是惊讶为什么从Java大师中没有人推荐使用简单的: 'public static final DBAccessService INSTANCE = new DBAccessService();' 正如@Kojotak也提到的那样。 它有漏洞吗?就我所了解和研究的这个问题,这绝对是正确的使用方法,但我困惑的是为什么它不是任何地方推荐的方法(我知道Joshua Bloch现在建议使用Enum来实现这个目的)。 - Aliaksandr Kazlou
谢谢。顺便说一句 - 如果您喜欢答案,通常使用问题分数旁边的向上箭头投票是礼貌的。我不需要声望,所以可以不用投票,但这表明对其他人有帮助。同样,如果您提出了问题并且它是您用来解决问题的答案,您可以单击复选标记以“接受”它。这有助于其他人知道答案有帮助。 - Nick Fortescue
2
@zshamrock:因为简单的 public static final ... = new ... 不是懒加载实例化。关键是要既懒加载实例化,又避免所有同步成本。唯一干净的方法是使用“按需初始化持有者类习惯用法”(如《Effective Java》中所命名和解释的那样)。请注意,使用Java 1.5的 volatile 有点违背了整个目的,因为即使您没有使用 synchronize/synchronized 关键字,您仍然会增加同步成本(volatile 也很昂贵)。所以,就是这样:懒加载+无同步:按需初始化类习惯用法 :) - SyntaxT3rr0r
1
@zshamrock - 我完全同意SyntaxT3rr0r的观点。如果你想使用懒加载实例化,请使用这种模式。如果你一定要每次都实例化,请使用枚举模式(急切实例化)。 - Nick Fortescue
@SyntaxT3rr0r 但是上面的方法不是有点懒吗?ResourceHolder 的静态字段 INSTANCE 不会在实际引用之前被初始化,对吧?请参见此处,“使其适用于静态单例” 我有什么遗漏的吗? - Stephan

2
在这篇文章中,它声称如果使用单独的Singleton类,“双重检查锁定”就不是问题。 点击此处查看原文
public class DBAccessHelperSingleton {
    public static DBAccessHelper instance = new DBAccessHelper(); 
} 

它具有相同的优势:在第一次引用之前不会实例化该字段。

如先前所述,如果您想保持现状,假设您仅针对JDK >= 5,则缺少volatile


+1 因为 Bill Pugh 既是一个有远见的人,又是一个天才 ;) 这个家伙 发明了 跳表。zomg :) 除此之外,与上面的注意事项相同:使用 volatile 会打败整个目的,这既是因为要偷懒,也是为了避免同步。 - SyntaxT3rr0r

1

它不是线程安全的,请查看一个优秀的文章。不能保证一个线程会看到完全初始化的DbAccessService实例。为什么不使用简单的

public class DBAccessService() {
     public static DBAccessService INSTANCE = new DBAccessService();
}

0

看起来不错。 如果两个线程调用getInstance()并且INSTANCE没有被初始化,只有一个线程能够继续执行createInstance(),而第二个线程将会发现实例不为null。

唯一需要注意的是INSTANCE关键字需要加上volatile关键字,否则Java可能会缓存它。


1
不,这段代码不太好。它与双重检查锁定一样存在问题。修复它的一种方法是使用第二段中解释的 volatile 关键字。 - Codo
你看到固定的答案了吗? - Vladimir Ivanov
2
@Vladimir:仍然不正确。Volatile 不是关于缓存,而是关于内存屏障。如果没有 volatile,线程可以看到非空的 INSTANCE 指向部分构造的对象。 - axtavt
请提供证明链接。我在这里看到了关于缓存的提及:http://www.javamex.com/tutorials/synchronization_volatile.shtml 和 http://en.wikipedia.org/wiki/Volatile_variable#In_Java - Vladimir Ivanov
@Vladimir:请查看WhiteFang34答案中的链接。还请参阅维基百科文章的第2点——自Java 5以来,volatile的语义是通过先于关系定义的,而不是通过缓存定义的。 - axtavt
好的,谢谢。我不会删除答案以保存这个评论串。 - Vladimir Ivanov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接