深入理解单例模式的三大要素(最容易忽略的构造函数)
详细的怎么写一个单例在此不过多赘述,推荐 单例模式的八种写法比较
下面以其中最经典的双重检查写法为例进行说明:
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
第一个要素
之前自己关注的重点一直在于getInstance获取单例的写法之中,可以理解为其仅仅是第一个要素,思想很好理解,先检查一次是否有被实例化,没有的话进行加锁,此时需要再通过一道检查,原因是要避免加锁语句产生的空隙,举个例子:A,B两个线程都访问到了第一次判断对象是否为null的语句,此时都检查为空,固其都能通过,即使有了加锁单例实例还是会被创建两次
第二个要素
另一个要素也不难理解,即将单例声明成volatile以防止指令重排序造成的意外情况,贴上《Java并发编程的艺术》中对其的解释:
固此处需要加volatile,依赖其添加的内存屏障以强制屏蔽编译器和JIT的优化工作。
第三个要素(很重要)
之前一直没有在意单例里边的构造函数
,网上的文章也很范范没有特别强调,自己写的时候很容易就忽略了。
再看一遍单例的实现,可以发现里面特意写了一个private的且为空的构造函数。
private Singleton() {}
为什么这里要特意加一个空的构造函数呢?如果没有这个构造函数会有什么问题吗?当仔细推敲这个代码,不难发现其中的蹊跷。
引用下昨天腾讯面试官问我的问题:我为什么一定要通过getInstance获取这个单例呢?我自己直接new一个不就有两个了吗?你怎么保证到单例的唯一性?(好问题,感慨鹅厂的前辈在一个问题的理解上真的很深)
抛出这个问题后再看这个构造函数问题就迎刃而解了,如果用默认的构造函数,那么new一个依然可以获取到另一份单例实体,这是不允许的。因此这里显示地把构造函数置空,同时加上了private保证外界无法访问,用户即使new了一个也无法再次创建单例对象,要用这个对象只能通过暴露给外界的getInstance方法获取
小结
单例模式最初的定义出现于《设计模式》:“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”,前面的两个要素仅仅是保证了提供了一个访问它的全局访问点(仅靠这两点还是伪单例),但无法保证该类只有一个实例,因此重写构造函数在唯一性的保证上显得尤为重要。
附上两点构造单例的要素:(特别容易忽视的第一点)
- 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
- 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
最后的效果如图:
通过private保证后,我们无法通过new来重新建一个单例对象,只能用getInstance方法获取