文章目录
- 什么是单例模式
- 单例模式的两种形式
- 饿汉模式
- 懒汉模式
- 懒汉模式与饿汉模式是否线程安全
- 懒汉模式的优化
什么是单例模式
单例模式其实就是一种设计模式,跟象棋的棋谱一样,给出一些固定的套路帮助你更好的完成代码。设计模式有很多种,单例模式是在校招当中最爱考的设计模式之一。
单例就指的是单个实例,一个程序如果频繁使用一个对象且作用相同,为了防止多次实例化对象,我们就可以使用单例模式,让类只能创建出一个实例,也就是一个对象,减少开销。
有一些场景本身就是要求某一个概念是单例的,例如JDBC里的DateSores
单例模式的两种形式
在Java中实现单例模式有很多种写法,我们这里重点讲解两种,懒汉模式与饿汉模式。
饿汉模式
饿汉模式,顾名思义,当人非常饿的时候,看见了食物,那种心情是怎么样的迫不及待。饿汉模式非常着急在类进行创建时就已经迫不及待的实例化单例对象了
class Singleton {
private static Singleton singleton = new Singleton();
public static Singleton getInstance() {
return singleton;
}
}
根据我们的描述可以写出这样的代码,但是我们发现,单例模式的初心我们并没有达到,单例模式的初心是让类只能实例化一次,此时我们并没有完成需求。我们通过私有化构造方法的方式来防止类的多次实例化:
class Singleton {
private static Singleton singleton = new Singleton();
public static Singleton getInstance() {
return singleton;
}
private Singleton () {}
}
此时我们单例模式中的饿汉模式就已经完成了,我们可以来测试一下:
懒汉模式
懒汉,所表示的含义并不是我们理解的流浪汉,相反懒表示的是一种从容不迫,是不着急,这种模式与饿汉模式的迫不及待不同,他只有在真正需要使用对象时才实例化单例对象。懒汉模式同样使用私有化构造方法的形式来完成初心,我们来写一下代码:
class SingletonLazy {
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstance() {
if(singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
return singletonLazy;
}
private SingletonLazy () {}
}
懒汉模式与饿汉模式是否线程安全
上面我们完成了懒汉模式与饿汉模式的代码编写,现在我们需要考虑一个问题,上面两个代码,是否线程安全,在多线程下调用getInstance()
是否会出现问题。
首先我们来看饿汉模式:
饿汉模式的getInstance()
方法为只读操作,所以在多线程下调用不会有什么问题,是安全的。
懒汉模式:
懒汉模式在多线程下,无法保证创建对象的唯一性。
例如两个线程同时调用getInstance()
方法,代码的执行顺序可能为:
1、线程一进行判断操作
2、线程二进行判断操作
3、线程一实例化对象
4、线程二实例化对象
这样线程一和线程二都会实例化对象,如果是N个线程可能会实例化N个对象,所以懒汉模式在多线程模式下不安全。
懒汉模式的优化
我们需要对懒汉模式进行优化,使得他在多线程下变得安全,如何操作呢?上面的分析中我们提到了,懒汉模式不安全的原因是,判断操作和new操作没有原子性,那么我们让他具有原子性不就可以了。我们就可以通过加锁来完成需求:
class SingletonLazy {
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstance() {
synchronized (SingletonLazy.class) {
if(singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
}
return singletonLazy;
}
private SingletonLazy () {}
}
这样就会有锁竞争,不会在出现向刚才那样两个线程同时进行判断的操作,一定是等一个线程new了之后,另一个线程才能竞争到锁进行判断。
我们觉得这样还是不够,不够高效,这样写虽然可以解决安全问题,但是同时也造成了效率的降低,每个线程都需要阻塞等待,但是我们分析一下,只有singletonLazy == null
时才需要进行阻塞,当singletonLazy != null
时其实就只是单纯的读操作。所以我们在进行优化:
class SingletonLazy {
private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstance() {
if (singletonLazy == null) {
synchronized (SingletonLazy.class) {
if(singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
private SingletonLazy () {}
}
这样又解决了我们的问题,上面代码中的两个判断条件看着是一样的,但是初心不一样,第一个是为了提高效率,判断是否需要加锁,第二个是为了判断是否需要实例化对象,两行代码看着离这不远,但是中间有一个加锁的操作,执行的时机其实差别很大。
这样就完了?并没有这里还有一个问题:指令重排序
什么是指令重排序呢?
创建一个对象,在jvm中会经过三步
1、创建内存空间
2、调用构造方法
3、将引用指向分配好的内存空间
我们发现,第二步和第三步好像可以进行交换执行顺序,交换之后对结果并没有影响,而这样不影响结果的情况下,可以不按照程序编码的顺序执行语句,提高程序性能的操作,我们称为指令重排序。
这里我们也实力化对象了,所以也可能有指令重排序的操作,例如线程一此时new对象的时候,发生了指令重排序,在没有调用构造方法的情况下进行了分配内存空间,此时系统调度到了线程二,线程二进行判断,此时引用非空就返回,这样我们返回了一个没有调用过构造方法的引用。
如何解决问题呢?我们使用volatile
就可以防止指令重排序:
class SingletonLazy {
volatile private static SingletonLazy singletonLazy = null;
public static SingletonLazy getInstance() {
if (singletonLazy == null) {
synchronized (SingletonLazy.class) {
if(singletonLazy == null) {
singletonLazy = new SingletonLazy();
}
}
}
return singletonLazy;
}
private SingletonLazy () {}
}
这样懒汉模式的优化,我们就完成了。