单例设计模式的写法有那么几种,懒汉式和饿汉式,但是相比较而言都不够严谨,存在逻辑漏洞,某些情况下并不能保证完全实现单例,尤其是在并发的情况下,会出现线程不安全的问题,这一点我们这里并不细讲,大家可以自行查找其他文章。
所以双判空加锁的单例写法就出现了,来看看一般人的写法:
public class Test {
private static Test instance;
private Test() {
}
public static Test getInstance() {
if (instance == null) {
synchronized (Test.class) {
if (instance == null) {
instance = new Test();
}
}
}
return instance;
}
}
乍一看,老铁没毛病,你还想双击666是不?
我们先来分析一下这段代码:
(1)构造函数使用private修饰,保证了其他类调用的时候不能通过这种方式初始化获得对象。
(2)getInstance方法中第一个判空条件,理论上是可以去除的,那为什么要加上呢?因为去掉了以后,不管有没有给instance赋值,都会进行synchronized加锁操作,但是synchronized操作会耗性能,所以第一次instance赋值后判空,避免了每次synchronized操作,提升性能。
(3)synchronized操作是为了避免多线程并发时出现instance多次赋值而达不到单例的效果,加同步后可以避免这个问题。
(4)第二个判空条件就不能去掉了,因为如果线程a和b同时操作,a先获得锁进入了,判空进入了代码,这个时候如果释放了锁b进入了判空条件先一步进行了初始化工作,这个时候a也进行了初始化工作,这就有了两个实例了,不符合要求,所以第二个判空条件不能去掉哦!
好,现在来说一下这种写法的问题:
问题主要在于instance = new Singleton()这句话并不是原子操作,原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
instance = new Singleton()其实做了三件事情:
(1)给instance实例分配内存;
(2)初始化Singleton()成员变量;
(3)将instance对象指向new Singleton()分配的内存空间,所以这个时候instance就不为null了;
问题就出在这儿了,因为JVM中有指令重排序的优化,所以呢正常情况按照1,2,3的顺序来,没毛病,但是也可能按照1,3,2的顺序来,这个时候就有问题了,调用的时候判断instance != null就直接返回instance实例,但是这个时候并没有进行初始化工作,所以在后续的调用中肯定就会报错了,所以这里引入了volatile
修饰符修饰instance对象,因为volatile能够禁止指令重排序的功能,所以能解决我们的这个问题,最后,写出完整+正确的单例双判空写法如下:
public class Test {
private volatile static Test instance;
private Test() {
}
public static Test getInstance() {
if (instance == null) {
synchronized (Test.class) {
if (instance == null) {
instance = new Test();
}
}
}
return instance;
}
}
其实如果说写到这儿,正常是没什么问题的,但是如果说非要挑刺儿的话呢,那就是我们被private修饰的构造函数并不安全,依然可以通过反射的方式创建一个新的对象,详情请跳转
《Java使用反射创建被private修饰的构造函数对象》查看,感谢!