什么是单例模式?
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。
我们为什么需要用到单例模式呢?
当我们的程序需要对某一个特定的对象进行很多次的操作的时候,如果每次都对这个对象进行创建,那么我们的程序的开销就会非常的大。为了避免对同一个对象多次进行重复的创建,我们就诞生出了单例模式,单例模式只会在内存中创建一次对象,程序需要这个对象的时候就可以直接调用单例模式中的对象,不需要重新创建对象,能够大大节省程序的开销。
单例模式的创建
单例模式有两种经典的实现方法,分别是饿汉模式和懒汉模式。
饿汉模式
在饿汉模式下的单例模式,在类加载的时候就会创建一个对象,也就是说,无论我们在程序中是否调用了这个单例模式,只要在饿汉模式的类加载了,这个对象就会被创建。
我们来看一下饿汉模式的代码:
class Singleton{
private Singleton(){};
private static final Singleton singleton = new Singleton();
public static Singleton getSingleton(){
return singleton;
}
}
因为饿汉模式是单例模式,所以不可以创建新的类对象,因此构造方法需要设置成private,这样就可以避免在程序中new出新的对象。
第三行可以解释饿汉模式下“在类加载的时候就会创建一个对象“这句话,因为创建的对象是Singleton的一个属性,所以就会在类加载的时候创建一个对象,无论有没有程序调用这个饿汉模式的代码,这个类都是客观存在的。
第三行代码的final可以保证这个对象不会被改变,能够保证我们的程序操作的永远是这一个对象。
在我们需要使用这个对象的时候,我们就可以直接通过类名.getSingleton获得到这个类。
懒汉模式
相比于在类加载的时候就创建对象的急性子饿汉来说,懒汉更倾向于程序调用它的时候再开始着手准备对象的创建。也就是说,懒汉模式和饿汉模式的区别就是是否在类加载的时候就创建了对象。
我们来看一下懒汉模式的简化版代码
class Singleton2{
private Singleton2(){};
private static Singleton2 singlenton2 = null;
public static Singleton2 getSingleton(){
if(singlenton2 == null){
singlenton2 = new Singleton2();
}
return singlenton2;
}
}
因为是单例模式,所以为了保证不创建新的对象,我们依然要把构造方法设置成为private
相比于饿汉模式,懒汉模式在创建引用类型的时候并没有给它分配内存空间,所以在类加载的时候是没有新建对象的,引用类型singleton2在类加载,没被程序调用的时候是指向一个null的。
在程序调用getSingleton的时候,先进行判断,如果对象没有被创建,我们就要进行创建,如果已经被创建了,说明程序不是第一次调用这个对象,在之前肯定已经创建好了,所以我们直接返回这个对象就好了。
多线程下的单例模式
单例模式肯定不会仅用于单线程,在多线程的情况下,我们如何保证单例模式的线程安全问题呢?
我们先来看一下啊饿汉模式和懒汉模式那个不用改动就可以实现线程的安全
答案是饿汉模式在上述代码的比较中更安全,因为在多线程的情况下,不同线程调用饿汉模式的getSingleton方法的时候,只能获得在类加载的时候就已经创建好的一个对象,不存在创建的情况,也就是说,饿汉模式下线程只能读,不能写,无论是线程的抢占执行还是代码顺序的改变,都只能读出饿汉模式的那唯一一个对象。
那么懒汉模式上述的代码为什么线程不安全呢?这是因为判断引用是否为空和新建对象操作不是原子性导致的。
我们先来看一下在正常情况下懒汉模式下创建一个对象的流程图
![](https://i-blog.csdnimg.cn/blog_migrate/5cd7ec1443360947f652132bd2fc20b3.png)
当线程变多之后,线程2在获取singleton2引用的时候,线程1还没有进行到新建对象的那一步,所以线程2获取的singleton2并没有因为线程1的调用新建出对象,反而线程2获取的singleton2还是指向null,所以线程1和线程2都会新建出对象,这样就不符合单例模式了。
![](https://i-blog.csdnimg.cn/blog_migrate/6c10cea9e8fbc1f816fc4d1ca6aa6d02.png)
为了解决这一原子性造成的问题,我们最好的方法就是给获取singleton2引用到新建对象之后加锁,使其成为一个整体,这样就能保证线程2获取的引用是线程1新建对象之后的引用了。
懒汉模式的代码更新成为:
class Singleton2{
private Singleton2(){};
private static Singleton2 singlenton2 = null;
public static Singleton2 getSingleton(){
synchronized (Singleton2.class){
if(singlenton2 == null){
singlenton2 = new Singleton2();
}
}
return singlenton2;
}
}
此时又出现了一个问题,我们对象的创建只会有一次,而对象的引用会被程序执行很多此次,那么每次引用的时候我们都要给他加上锁吗?我们只有在程序第一次调用的时候才会进行创建对象(写)操作,会发生线程安全问题,但是在之后的程序调用过程中会变成与饿汉模式一样的只读模式,所以不需要每次都加锁,创建对象之后每次加锁只会增加开销而已,毫无用处,所以我们要在锁的外面在进行一次判断,如果获得到的引用不是null就直接返回。
class Singleton2{
private Singleton2(){};
private static Singleton2 singlenton2 = null;
public static Singleton2 getSingleton(){
if(singlenton2 == null){
synchronized (Singleton2.class){
if(singlenton2 == null){
singlenton2 = new Singleton2();
}
}
}
return singlenton2;
}
}
懒汉模式的多线程标准实现
我们上述的懒汉模式的代码看似已经完整,其实还忽略了几个问题,线程安全问题不仅仅只有原子性的问题,还会涉及到内存可见性和指令重排序的问题。
我们可以把懒汉模式下的创建过程分为以下的三步
为singleton2分配内存空间
初始化singleton2对象
将singleton2指向分配好的内存空间
JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能
在单线程的情况下这是一步优化,是好事,但是在多线程的情况下,指令的重排序就会带来致命的问题。
正常情况下线程按照a,b,c的顺序进行,但是指令重排序就会导致按照a,c,b的顺序执行,有可能线程1执行a,c之后,线程2进行判断的时候就会出现singleton2已经被分配了空间但是还没初始化对象的情况,就会报NPE的错误,所以使用volatile修饰变量就可以防止指令重排序的问题,顺便也防止了内存可见性的问题。
以下是懒汉模式的多线程的标准实现。
class Singleton2{
private Singleton2(){};
private static volatile Singleton2 singlenton2 = null;
public static Singleton2 getSingleton(){
if(singlenton2 == null){
synchronized (Singleton2.class){
if(singlenton2 == null){
singlenton2 = new Singleton2();
}
}
}
return singlenton2;
}
}
总结
饿汉模式和懒汉模式的区别就是“类加载的时候是否新建了对象”,饿汉模式天生是线程安全的,但是因为在类加载的时候就创建了类,如果程序没有调对象就会导致多余开销,所以就使用懒汉模式。而懒汉模式线程不安全,是原子性,内存可见性和指令重排序造成的,因此我们给其加锁和加volatile关键字就可以解决。