单例也就是只能有一个实例,即只创建一个实例对象,不能有多个。
可能会疑惑,那我写代码的时候注意点,只new一次不就得了。理论上是可以的,但在实际中很难实现,因为你无法预料到后面是否会脑抽一下~~因此我们就可以通过编码技巧,让编译器来帮助我们检查约束。
单例模式又分为两种:饿汉式和懒汉式,区别在于实例对象创建的时机不同。
0x00 饿汉式
说到饿汉,那一定是很饥饿的人,很急切,也就是说这个实例对象创建的时机比较早,在类加载的时候就创建了。
要点:
-
1、由于创建时机较早,设置为静态即可在类加载的时候就可以创建实例对象
-
2、由于只能创建一个实例对象,因此将构造函数私有化
-
3、对外提供一个获取实例对象的静态类方法
public class Singleton{ private static Singleton instance = new Singleton(); public static Singleton getInstance(){ return instance; } private Singleton(){ } }
0x01 懒汉式
懒汉,顾名思义,很懒,比较缓慢,也就是说这个实例对象是在使用到的时候才去创建。
要点:
-
1、一开始设置为null,直到在使用的时候再去new实例对象
-
2、由于只能创建一个实例对象,因此将构造函数私有化
3、对外提供一个获取实例对象的静态类方法
public class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
0x02 单例模式与多线程
众所周知,许多在单线程环境下的代码一到多线程环境下就会出错,在此将对单例模式在多线程下的编写进行调整优化~
先来看看饿汉式的代码,我们发现这个实现的方式是在类加载的时候就去实例化对象了,而后续在多线程中掉用getInstance方法来获取实例对象的时候,只会进行返回操作,即”只读”,因此多个线程在操作的时候是安全的。
再来看看懒汉式,可以发现在调用getInstance方法的时候,会进行修改操作,如果恰巧有两个线程同时进入了if,然后就会进行new操作,那不就创建了多个实例对象了,如果恰巧每个对象不是很轻量,可能有很多属性,加载了几十G的数据到内存中......
因此解决方法是进行加锁操作,保证if和new操作是原子的。
public class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
synchronized(SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){
}
}
这样修改以后,就能保证if和new操作是一个整体,此时线程的安全问题就得到了改善。
但是这样写是否就真的完了呢?
答案:不是。因为我们是单例模式,所以只需要new一次之后,实例对象就不可能是null了,后续直接return对象就行了。假如我们现在已经创建好了一个对象,当后续有多个线程去调用getInstance方法的时候,需要去获取到这把锁才能进行返回操作,也就是我们的多线程变成了串行化,并发程度几乎没了~
那是否有办法,既可以保证代码线程安全,又不会对执行效率产生大的影响呢?
如下代码:
public class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
synchronized(SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
与前面的代码对比,可以发现,在外层多加了一个if语句。
分析:
当有多个线程进入了第一个if语句,此时里面有一个加锁操作,这样可以保证只创建了一个实例对象。假设此时已经创建好了一个实例对象了,当下一次又有多个线程进来时,由于instance不为null,直接返回了,并不会进入到加锁操作,此时并发并没有再受到影响了,既保证了正确又保证了效率。
总结:
在锁外面的if语句保证了是否要进行加锁操作,即如果还没有创建实例对象,此时线程由于需要进行修改操作要进行加锁才能保证安全,如果已经创建了实例对象,此时线程安全了就不用加锁了。
在锁里面的if语句是来保证只创建一个实例对象。
0x03 指令重排序
上述代码其实还不够安全,还存在指令重排序的问题。
指令重排序,本质是编译器为了提高执行效率,调整原有的代码执行顺序(前提是保持逻辑不变)。
接下来具体问题具体分析:
上述的代码中,出现了一个new操作,而new操作其实是分三步的,第一步,申请内存空间,第二步,在内存空间上构造对象,第三步,把内存地址赋值给对象引用。一般是按照1,2,3的顺序执行的,但是编译器优化过后,可能会按照1,3,2的顺序执行,在单线程环境下都可以,但是在多线程环境下则不是。如果线程1是按照1,3,2顺序执行完了3后,线程2此时返回了这个实例对象,但是此时这个对象还没有初始化呢,也就是说这是个非法对象,当我们的线程2去访问这个对象的属性或方法时,此时就是非法,会出错。此时我们还需要再对instance加一个限制操作,即用volatile来修饰instance。
最终代码:
public class SingletonLazy {
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
synchronized(SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){
}
}
至此,这段“健壮”的代码基本可以满足使用了~