什么是单例程?
其实啊,单例程是一种设计模式。
那啥是设计模式呢?
设计模式就好比象棋中的棋谱。假如,红方当头炮,那么黑方就马来跳,这样就能有效应对红方的走棋。因此黑方应招的时候有一些固定的套路,按照套路走,局势就不会吃亏。
在软件开发中也有很多常见的问题场景,针对这些问题场景,大佬们总结出了一些固定的套路,按照这个套路来实现代码,就不会吃亏。由此可见啊,那些大佬为了我们可谓是操碎了心。
单例程的作用:
它能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例。
常见的实现方式:“饿汉模式”和“懒汉模式”
饿汉模式:
饿汉模式的本质就是:在类加载的同时创建实例。
//饿汉模式的实现:
class Singleton{
//static成员的初始化是在类的加载的时候,此处可以简单的认为,就是jvm一启动,就立即加载,可见这个对象创建的时机很早。
private static Singleton instance = new Singleton();
//此处将构造方法设置为private修饰,是为了防止当在外部有人想再new一个对象的时候,会产生报错
//产生报错的原因很简单,就是new的时候会调用构造方法,而构造方法却是private修饰的,不允许访问,所以会发生报错
private Singleton(){}
//可以通过getInstance这个方法,来获取已经new好的对象
public static Singleton getInstance(){
return instance;
}
}
洁净板:
class Singleton{
private static Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
上面代码中的解释想必已经很详细了,但是,我们再想一个问题:假如我把这个Singleton类改了,就在这个类里面new多个实例,要知道我们目的就是:为了防止创建多个实例。这样一来不是有点矛盾了吗
懒汉模式:
其实啊,“懒”这个字在计算机种并不是一个贬义词,而是一个褒义词,为什么呢?想一想如果你此时正在看电视,你口渴了想去喝水,但是你又懒的去的,于是你就没动,但是,过了一会你又饿了,你不得不去弄点吃的,然后顺便就把水喝了。这样你需要跑两趟的动作,一趟就完成了,是不是提高了效率捏。
本质:什么时候用到了再创建,用不到就不创建
//懒汉模式的实现:(初级)
class Singleton{
public static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
private Singleton(){}
}
两种模式的安全性问题(多线程)
- “饿汉模式”是线程安全的。
- “懒汉模式”是线程不安全的。
原因:饿汉模式之所以安全是因为:
懒汉模式之所以不安全是因为: - 线程安全问题发生在首次创建实例时,如果在多个线程中同时调用getInstance方法,就可能导致创建出多个实例。
出现了线程不安全,我们的第一反应就是加锁。
class Singleton{
public static Singleton instance = null;
private static Object locker = new Object();
public static Singleton getInstance(){
if(instance == null){
synchronized(locker){
instance = new Singleton();
}
}
return instance;
}
private Singleton(){}
}
- 进一步改进:
class Singleton{
public static Singleton instance = null;
private static Object locker = new Object();
public static Singleton getInstance(){
//将if和new打包成一个原子操作
synchronized(locker){
if(instance == null){
instance = new Singleton();
}
}
return instance;
}
private Singleton(){}
}
第二步改进:
注意
- 上面的第一个 if 的作用是判断是否需要加锁,防止实例化以后,已经不需要加锁了,但是还是重复加锁的繁琐操作,防止低效。
- 第二个 if 的作用是判断当前是否创建了实例,两个条件虽然相同但是含义不同。
举个例子:
第三步优化:
我们还要考虑内存可见性和指令重排序的问题:加上volatile就行了
//经过三步优化,这才是最安全的代码:
class Singleton{
//加上volatile能避免内存可见性(一成) 和 指令重排序的问题(九成)
public static volatile Singleton instance = null;
private static Object locker = new Object();
public static Singleton getInstance(){
//双if + 锁 更安全
if(instance == null){
synchronized(locker){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
private Singleton(){}
指令重排序:
接下来我们就着重说一说指令重排序的问题,先举个例子:假如下面是一个菜市场的地图,我们需要去买几样菜给出几个买菜的先后顺序:
1)西红柿——》鸡蛋——》茄子——》黄瓜
2)黄瓜——》西红柿——》茄子——》鸡蛋
你觉得哪一个路线效率更高呢?
很明显肯定是 2
指令重排序就是把要执行的代码顺序调整后,最终的效果没变,但是效率提高了不少,重排序的前提一定是重新排序之后,逻辑和之前等价
- 单线程下,编译器进行指令重排序的操作,一般都是没问题的,编译器可以精确的识别出,哪些操作可以重排序,而不会影响到逻辑。
- 多线程下,判定可就不准确了,就有可能会出现重新排序后,逻辑发生了改变了——》引起bug
面试小技巧: