如何编写线程安全的getInstance方法?

joh*_*ohn -1 java multithreading locking thread-safety

我的getInstance库中有一个在多个线程中使用的方法,但我不确定它是否是线程安全的:

protected static DataClient instance;

protected DataClient() {
    // do stuff here
}

public static synchronized void initialize() {
    if (instance == null) {
        instance = new DataClient();
    }
}

public static DataClient getInstance() {
    if (instance == null) {
        initialize();
    }
    return instance;
}
Run Code Online (Sandbox Code Playgroud)

这就是我使用它的方式:

DataClient.getInstance();
Run Code Online (Sandbox Code Playgroud)

这个线程是否安全,如果不是,那么谁能解释为什么它不是线程安全的?

Gra*_*ray 7

这个线程是否安全,如果不是,那么谁能解释为什么它不是线程安全的?

它不是线程安全的,因为您实际上是在执行双重检查锁定错误。在完全实例化之前,没有什么可以保护它instance不被发布。仅仅因为is并不意味着它不会在同步块结束之前发布,这可以在非同步方法中看到。因此,线程调用可能会获得对部分初始化实例的引用。initialize()synchronizedinstancegetInstance()getInstance()DataClient

此外,即使创建线程发布它,也不能保证另一个线程会更新与该对象关联的内存。缓存内存可能会导致部分对象和其他关键内存问题。

以下是一些使其安全的方法:

  • 最简单的方法是 make instancebe volatile。这会强制在写入时跨越写内存屏障,instance并在访问时跨越读取内存屏障。这修复了双重检查锁定错误。
  • 您可以在同步完成的类加载时实例化对象:

    protected static final DataClient instance = new DataClient();
    
    Run Code Online (Sandbox Code Playgroud)
  • 你可以使用AtomicReference但只是包装一个volatile对象,所以制作instance volatile是相似的。

  • 您可以确保中的所有字段DataClient都是finalorvolatile以便在构造函数完成时完全构造对象。尽管仍然是一个糟糕的模式,但我相信这会阻止内存模型在构造对象重新排序构造函数初始化。欲了解更多信息,请阅读此页面

例如,假设您有 2 个线程。线程1调用getInstance()instancenull。它调用initialize()which 构造DataClient并将其发布到static instance变量。然后它到达synchronized块的末尾,因此instance被发布到中央存储器。

Thread2 现在调用getInstance()并获取引用,instanceinstance缓存了一些现在已过时的内存部分。因为它没有跨越读内存屏障,所以没有强制 Thread2 更新其内存缓存以确保instance已正确共享的机制。它可能instance只看到部分字段更新或没有字段更新。发布者和消费者线程必须应用内存同步,否则可能会发生内存争用情况。