在使用双重锁定时,将单例实例设为volatile的目的是什么?
在使用单例模式时,为什么要将单例实例设置为volatile并使用双重锁定?
单例模式是一种常用的设计模式,它确保一个类只有一个实例,并提供全局访问点。在多线程环境下,单例模式的实现必须保证线程安全,以防止多个线程同时创建多个实例。
在使用双重锁定机制实现线程安全的单例模式时,需要将单例实例设置为volatile。这是因为在Java中,对一个volatile变量的写操作会在写操作完成之前,先将值刷新到主内存中,而对volatile变量的读操作会从主内存中读取最新的值。在多线程环境下,如果没有将单例实例设置为volatile,则可能出现指令重排序的情况,导致一个线程在检查到单例实例不为空之后,获取到的实例可能还没有完成初始化,从而导致程序出现错误。
为了解决这个问题,可以使用双重锁定机制和volatile关键字。在双重锁定机制中,首先判断单例实例是否为空,如果为空,则进行同步操作,确保只有一个线程可以进入临界区。在进入临界区后,再次检查单例实例是否为空,如果为空,则进行实例化操作。使用volatile关键字可以确保在实例化操作完成之前,其他线程无法获取到未初始化的实例。
以下是使用双重锁定机制和volatile关键字实现线程安全的单例模式的示例代码:
public class Singleton { private volatile static Singleton instance; private Singleton() { // 私有构造函数 } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
通过将单例实例设置为volatile,并使用双重锁定机制,可以确保在多线程环境下安全地创建单例实例。这样可以避免多个线程同时创建多个实例的问题,保证了单例模式的正确性和线程安全性。
在多线程环境下,如果没有使用volatile
关键字,代码将不能正确运行。根据维基百科的双重检查锁定( Double-checked locking):
“从J2SE 5.0开始,这个问题已经被修复。volatile关键字现在确保多线程正确处理单例实例。这种新的用法在《双重检查锁定是错误的》声明中描述。”
// 使用volatile的获取/释放语义工作正常 // 在Java 1.4和更早版本的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; } // 其他函数和成员... }
通常情况下,应尽量避免使用双重检查锁定,因为很难正确使用,如果使用不当,很难找到错误。可以尝试使用以下更简单的方法:
“如果帮助对象是静态的(每个类加载器一个),另一种选择是延迟初始化持有者模式。”
// 正确的Java延迟初始化 class Foo { private static class HelperHolder { public static Helper helper = new Helper(); } public static Helper getHelper() { return HelperHolder.helper; } }
双重检查锁定在某些情况下是必要的,例如,当您拥有一个在运行时可能发生变化但在程序中只能存在一次的单例时,比如登录会话。您可以使用它来访问网站,但是您必须不时重新连接并获取新的会话。但是,如果值在运行时不会改变,应避免使用双重检查锁定。
在持有者模式中,您如何传递参数给构造函数?
synchronized
块是否确保将检索到非缓存值的字段,这意味着volatile
部分不再需要吗?即使在今天是否仍然需要?
在使用双重锁定时,为什么要将单例实例的实例变量volatile呢?
原因:
当一个线程发现uniqueInstance为null时,它会锁定,确认uniqueInstance仍然为null,然后调用单例的构造函数进行初始化。构造函数会对Singleton中的成员变量XYZ进行写操作,并返回。线程A现在将对新创建的单例的引用写入uniqueInstance,并准备释放锁。
就在线程A准备释放锁的时候,线程B出现了,并发现uniqueInstance不为null。线程B访问uniqueInstance.XYZ,以为它已经被初始化了,但由于CPU重新排序了写操作,线程A写入XYZ的数据对线程B来说还不可见。因此,线程B看到的XYZ中的值是错误的。
解决方法:
将uniqueInstance标记为volatile,插入了一个内存屏障。在修改uniqueInstance之前,所有在uniqueInstance之前发起的写操作将会完成,从而防止了上述重新排序的情况发生。
具体来说,这两个重新排序的写操作是:1)A给uniqueInstance分配内存地址,2)XYZ得到一些有意义的值。
"because the CPU has reordered writes...."的意思是什么?
这是字面意思,如果你的代码在A之前写入位置B之前,使用volatile可以确保A在B之前被写入。如果没有volatile,CPU可以在A之前写入B,只要对代码逻辑没有可检测的影响。
“Just as thread A gets ready to release its lock”真的是重要的吗?
是的,这是重要的,因为在线程A释放锁之前,线程B可能会访问uniqueInstance,从而导致上述重新排序的问题。
"prevents memory writes from being re-ordered"的意思是什么?
字面意思就是:如果你的代码要求在位置A之前写入位置B,在使用volatile的情况下,可以确保A在B之前被写入。没有使用volatile,CPU可以在A之前写入B,只要对代码逻辑没有可检测的影响。
如果实例有两个变量,XYZ被初始化而DEF没有被初始化,在DEF开始之前,另一个线程来了并看到非null值并尝试访问DEF,由于之前的线程没有尝试启动DEF的写操作,volatile会怎么帮助?
其他线程直到单例指针被存储之前无法看到实例。
但是一旦你创建了内存,instance= new instance,你把它初始化了,现在t1这样做并且还做了instance.XYZ=123,下一条语句是instance.DEF,在执行DEF语句之前,它在完成instance.XYZ之后被抢占了,现在CPU没有任何未完成的写操作,因为线程没有尝试启动instance.DEF,如果线程2来读它会怎么样?
你不是给instance赋值`instance= new instance`,你是给一个本地变量`tmp = new instance`赋值,然后再次检查,最后执行`instance = tmp`。请参考已接受答案中Mark的代码。
太复杂了,我们可以只使用静态变量`private static final Foo INSTANCE = new Foo()`,然后让getInstance只返回this而不是使用volatile和所有的线程安全吗?我看到一些帖子建议这种方式。
这种方式是可行的,但是它不是懒加载的,也就是说在程序启动时就会创建实例。
如果在赋值变量之前手动调用`Thread.MemoryBarrier();`会有帮助吗?
哦,这是Java,不是C#,我弄错了。