单例模式
介绍:单例模式是一种设计模式。
单例=单个实例(对象),某个类,在一个进程中,只应该创建一个实例,使用单例模式,就可以对代码进行更严格的校验和检查。
唯一的对象如何保证:首先,人一般是不可靠的,所以要通过机器来对代码中指定的类创建的实例个数进行校验,如果发现个数多了,就直接编译报错这种。
实现单例模式的方式有很多种,这里介绍两种最基础的实现方式:
1)饿汉模式
2)懒汉模式
1.饿汉模式
因为Singleton()是private修饰的,除此之外,static指的是“类属性”,instance就成为Singleton的一个类对象,Singleton.class是唯一的,所以无法new新的对象。其他代码就只能通过Singleton的getInstance方法来获取现成的对象。
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
private Singleton(){}
}
所谓“饿”形容“非常迫切”,实例是在类加载的时候就创建了,创建时机非常早,相当于程序一启动,实例就创建了,就用饿汉形容“创建实例非常迫切,非常早”。
2.懒汉模式
创建实例的时间:在第一次使用的时候。
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance==null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){};
}
如果是首次调用getInstance,此时instance为null,就会进入if条件,从而把实例创建出来。如果是后续再次调用getInstance,由于instance已经不是null,就直接返回之前创建好的引用。而且和饿汉模式一样,也将构造函数设置成了私有。
这样就保证该类的实例是唯一一个,创建实例的时机就调整了为第一次调用getInstance的时候。
单例模式在多线程中的应用
一个关键问题:懒汉模式和饿汉模式是否是线程安全的?
对于饿汉模式,getInstance直接返回Instance实例,这个操作本质上就是“读操作”,而多个线程读取同一个变量,显然是安全的。
对于懒汉模式,即有读又有写,在一个线程创建好实例后,另一个线程仍认为是null,也会创建一个实例,所以懒汉模式是不安全的,可能会创建出多个实例。
如何改变懒汉模式,让它是安全的?
可以通过加锁,将if和new打包成为一个操作。
class SingletonLazy{
private static SingletonLazy instance = null;
private static Object lock = new Object();
public static SingletonLazy getInstance(){
synchronized (lock) {
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
private SingletonLazy(){};
}
但是如果Instance已经创建过了,后续再调用getInstance就是直接返回Instance实例了,这时已经是纯粹的读操作了,也就不存在线程安全问题了,此时在对已经没有线程安全问题的线程加锁,每次都要加锁解锁,效率会变很低。所以锁不能乱加。
所以要在代码外面再加一个判断条件。
class SingletonLazy{
private static SingletonLazy instance = null;
private static Object lock = new Object();
public static SingletonLazy getInstance(){
if(instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){};
}
这样两个if判断条件相同,看起来有点怪,但是这在多线程里是很正常的。
但是,代码仍然还有问题。
指令重排序引起的线程安全问题。
指令重排序,也是编译器优化的一种方式:调整原有代码的执行顺序,保证逻辑不变的情况下,提高程序的效率。
instance = new SingletonLazy();
最容易出现问题的就是这句代码。
宏观上,它可以分为三个步骤:
1)申请一段内存空间。
2)在这个内存上调用构造方法,创建出这个实例。
3)把这个内存地址赋值给instance引用变量。
正常情况下,上述代码是按照 1 2 3 的顺序执行的,但是编译器可能会优化为1 3 2 来执行。单线程情况下,这两种顺序都是可以的。都是多线程就可能会有问题了。
假如有线程t1和t2,t1执行完13后调度走了,这时t2执行132,因为instance!=null了,t2就会把一块空地址误认为是已经构造好的instance,使用Instance中的方法属性就会导致错误。
解决上述问题的方法,还是volatile。
volatile有两个功能:
1)保存内存可见性。(前文已经提到了)
2)禁止指令重排序,被volatile修饰的变量的读写操作的相关指令都不会被重排序。
private volatile static SingletonLazy instance = null;
instance被volatile修饰后就不会重排序了。
class SingletonLazy{
private volatile static SingletonLazy instance = null;
private static Object lock = new Object();
public static SingletonLazy getInstance(){
if(instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy(){};
}
最后,这段经典面试题的代码就出现了。