答案是肯定的
要说明具体原因,需要先了解以下几点。
一.对象的创建过程:
以Object的创建作为例子,创建对象一共五个步骤:
- new:分配内存。
- dup:复制一份刚刚创建的内存空间的引用,并压栈。(此时栈中有两个内存空间的引用)。
- invokespecial:取出一个引用,调用init方法,这里就指构造函数,到这一步,完成了对象的new Object()过程。
- astore_1:将栈顶的引用的值赋值给局部变量表角标为1的变量。当前是main方法,局部变量有两个,角标为0的是args,角标为1的是object。这一步是完成对象的初始化。
- return:Return
voidfrom method。
二.volatile的作用:
- 保证线程的可见性:本质上是使用了CPU的缓存一致性协议。缓存一致性协议有很多,比如MESI、MOSI、MSI等。inter的cpu使用的MESI(M:Modified、E:Exclusive、S:Shared、I:Invalid )。
- 禁止指令重排序:使用了内存屏障。在JVM层,volatile修饰的变量在进行写操作前加了StoreStoreBarrier,之后加了StoreLoadBarrier;读操作前加了LoadLoadBarrier,之后加了LoadStoreBarrier。在硬件层面,Windows是通过lock指令实现的。
三.为什么DCL单例需要加volatile:
public class SingletonDoubleCheckTest {
private static volatile SingletonDoubleCheckTest INSTANCE;
private SingletonDoubleCheckTest() {
}
public static SingletonDoubleCheckTest getInstance() {
if (null == INSTANCE) {
synchronized (SingletonDoubleCheckTest.class) {
if (null == INSTANCE) {
INSTANCE = new SingletonDoubleCheckTest();
}
}
}
return INSTANCE;
}
}
多个线程执行getInstance()方法,第一个线程获得锁,在执行创建对象的过程中,invokespecial和astore_<n>进行了指令重排序(根据hanppens-before原则,上面这两条指令是有可能进行指令重排序的。),第一个线程执行完astore_<n>时,此时INSTANCE已经不为空了,有一个引用指向了它,只不过这块内存的内容是NULL,此时第二个线程执行方法,在判断第一个null == INSTANCE,因为INSTANCE已经有地址了,第二个线程不会再继续争抢锁,直接将这个对象返回,这样会出现错误,因为对象并没有创建完成。
如果加了volatile,就会保证先进行初始化,再赋值给变量。
总结:在《Effective Java 第三版》中说道:实现单例的一种方法是声明一个包含单个元素的枚举类型,因为即便是在面对复杂的序列化或者反射攻击的时候,也可以防止多次实例化。我个人还没有在项目中使用过枚举类型创建单例,我比较常用的是懒汉式的,具体,只能是因项目而异了。
DCL(双重检查锁定)单例模式在多线程环境下需要使用volatile关键字,以确保对象创建的正确性。volatile保证了线程间可见性和禁止指令重排序,防止多个线程在对象未完全初始化时获取到不完整的实例,从而避免错误的发生。此外,文章提到了《Effective Java》中推荐使用枚举类型实现单例,但实际应用中会根据项目需求选择不同的实现方式。

841

被折叠的 条评论
为什么被折叠?



