在使用双重锁定时,将单例实例设为volatile有什么意义?

46
private volatile static Singleton uniqueInstance

在单例模式中,使用双重锁定方法进行同步时,为什么需要将单个实例声明为volatile?如果不声明为volatile是否可以实现相同的功能?


关于为什么首先需要双重检查的相关帖子,请参见此处此处 - RBT
5个回答

76

volatile关键字可以防止内存写入被重新排序,这样其他线程就不可能通过单例的指针读取未初始化的字段。

考虑这种情况:线程A发现uniqueInstance == null,锁定并确认它仍然是null,然后调用单例的构造函数。构造函数将在Singleton内部的成员XYZ中进行写操作,然后返回。现在,线程A将对新创建的单例的引用写入uniqueInstance,并准备释放锁。

就在线程A准备释放锁的时候,线程B出现了,并发现uniqueInstance不为null。线程B访问uniqueInstance.XYZ,认为已经被初始化,但由于CPU已经重新排序写操作,因此线程A写入XYZ中的数据还没有对线程B可见。因此,线程B看到的XYZ中的值是错误的。

当你使用volatile标记uniqueInstance时,会插入一个内存屏障。在修改uniqueInstance之前启动的所有写操作都将完成,从而防止上述重新排序的情况。


4
谢谢你的回答。 - Abhijit Gaikwad
1
具体来说,这两个被重新排序的写入操作是:1)A将内存地址分配给 uniqueInstance,2)XYZ得到了有意义的内容。 - lcn
1
@dasblinkenlight:你说过“因为CPU重新排序了写入操作……”。请解释一下重新排序写入操作是什么意思? - tharindu_DG
2
这意味着,字面上来说,如果你的代码在写入位置B之前要求写入位置A,使用“volatile”可以确保A实际上是在B之前被写入的。如果没有“volatile”,CPU可以在A之前写入B,只要它对代码逻辑没有可检测的影响即可。 - Sergey Kalinichenko
@user124,你不应该直接赋值 instance = new Instance(),而是应该先定义一个本地变量 tmp = new Instance(),然后再进行检查,最后再将其赋值给 instance。可以参考 Mark 在被采纳的答案中的代码。 - Sergey Kalinichenko
显示剩余8条评论

34

没有使用volatile关键字,代码在多线程环境下无法正确工作。

来自维基百科的双重检查锁定

从J2SE 5.0开始,这个问题已经被修复。现在使用volatile关键字可以确保多个线程正确处理单例实例。这种新的用法在“双重检查锁定已经过时”声明中有描述:

// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null) {
                    helper = result = new Helper();
                }
            }
        }
        return result;
    }

    // other functions and members...
}

一般情况下,如果可能的话,请避免使用双重检查锁定,因为很难正确实现,如果实现不正确,很难找到错误。请尝试使用这种更简单的方法:
如果辅助对象是静态的(每个类加载器一个),则可以使用按需初始化占位符
// Correct lazy initialization in Java 
@ThreadSafe
class Foo {
    private static class HelperHolder {
       public static Helper helper = new Helper();
    }

    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

3
在某些情况下,双重检查锁定可能是必要的。当您有一个单例在运行时可以发生变化,但在程序中只能存在一次时,例如登录会话,您希望使用它来访问站点,但不时需要重新连接并获取新的会话时。然而,如果该值在运行时不会更改,则应避免使用双重检查锁定。 - Xtroce
1
在使用holder惯用法时,你将如何传递参数给构造函数? - Aamir Rizwan
synchronized 块不是确保检索非缓存字段的值吗?这意味着 volatile 部分不再需要了吗?即使今天,这仍然是必需的吗? - android developer

11
为了避免使用双重锁定或volatile,我使用以下方法
enum Singleton {
     INSTANCE;
}

创建实例很简单,采用惰性加载方式并且是线程安全的。


2

对于volatile字段的写操作会发生在任何读操作之前。 以下是一个示例代码,以便更好地理解:

private static volatile ResourceService resourceInstance;
//lazy Initialiaztion
public static ResourceService getInstance () {
    if (resourceInstance == null) { // first check
        synchronized(ResourceService.class) {
            if (resourceInstance == null) { // double check
                // creating instance of ResourceService for only one time
                resourceInstance = new ResourceService ();                    
            }
        }
    }
    return resourceInstance;
}

这个链接可以给你更好的服务 http://javarevisited.blogspot.com/2011/06/volatile-keyword-java-example-tutorial.html


此链接是关于Java中volatile关键字的示例和教程,有助于您更好地理解该技术。

-4
你可以使用以下代码:
private static Singleton uniqueInstance;

public static synchronized Singleton getInstance(){
    if(uniqueInstance == null){
        uniqueInstance = new Singleton();
    }
    return uniqueInstance
}

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