设计模式-单例模式

介绍

单例模式可以说是很多开发者解除到的第一个设计模式,当然也可能用到了自己没发觉是设计模式,单例模式的核心思想莫过于创建类的唯一实例,从而避免类的重复创建,达到节约资源开销的目的,从而满足业务上的实际需求。

定义

确保某个类有且只有一个实例,且只有自己可实例化

饿汉模式

这是最浅显的写法,直接上代码

public class Person {

    public static Person mInstant = new Person();
    public static Person getInstance(){
        return mInstant;
    }
    private Person() {
    }
}


这是最普通的单例模式,缺点也很明显就是类的实例在一开始就创建了,难免浪费资源,理想状况当然是使用的时候再创建,于是便有了懒汉模式

懒汉模式

话不多少,先上代码

public class Person {
    private static Person mInstant = null;

    public static Person getInstance(){
        if (mInstant == null) {//1
            mInstant = new Person();//2
        }
        return mInstant;
    }
    private Person() {
    }
}

可以看到,就是在实际用到的时候才去实例化,这样确实可以省内存开销,并且在单线程运行下是没有问题的,多线程就不一定了。假设有A、B两个线程同时调用getInstance(),此时都走到第1步,mInstant还没有初始化,所以AB两个线程会拿到两个Person对象,这就与我们单例模式的定义不符。大家可能会认为这种概率比较小不用考虑,实际中并发编程是我们用的最多的,小概率也是代表会发生,在不考虑机器出错的情况下,程序员的使命就是应该让程序尽可能无bug。所以这时候关键字synchronized

public class Person {
    private static Person mInstant = null;

    public synchronized static Person getInstance(){
        if (mInstant == null) {
            mInstant = new Person();
        }
        return mInstant;
    }
    private Person() {
    }
}

或是

public class Person {
    private static Person mInstant = null;

    public static Person getInstance(){
        synchronized (Person.class) {
            if (mInstant == null) {
                mInstant = new Person();
            }
        }
        return mInstant;
    }
    private Person() {
    }
}

实际上这两种方式大同小异,是能够达到多线程只有一个实例的目的,但缺点也很明显,就是效率太低了,试想一下,每次调用方法都需要synchronized 一次,那对性能会有很大的影响,所以就有了DCL(double checked locking)单例模式

DCL单例

public class Person {
    private static Person mInstant = null;

    public  static Person getInstance(){
        if (mInstant == null) {//1
            synchronized (Person.class) {//2
                if (mInstant == null) {//3
                    mInstant = new Person();
                }
            }
        }
        return mInstant;
    }
    private Person() {
    }
}

可以看到DCL模式使用了双重校验,即使在多线程中线程AB都已经到2,此时A得到线程锁开始实例化,之后释放锁,线程B再进来也会判空校验,从而避免了创建多个对象的情况。
那么这样就万无一失了嘛?答案是否定的,因为编译器会对指令进行优化排序(优化排序指的是编译器在不改变单线程语义的情况下,可以重新安排程序的执行顺序)。
我们来看下new一个对象的时候,优化排序前会进行如下操作:

1.分配一块内存M
2.在内存M上实例化Person
3.将内存M地址赋予mInstant

经过优化排序后,编程如下:

1.分配一块内存M
2.将内存M地址赋予mInstant
3.在内存M上实例化Person

在单线程中,这样确实没有改变语义并且运行结果也是预期中,但在多线程中会有问题,上图:
在这里插入图片描述
在图中,如若A在内存M赋值时,线程B进行了非空判断,要知道mInstant == null比较的就是内存地址,而null内存地址默认为000000,而B拿到了M的内存比较,直接返回了mInstant,此时B使用的对象进行操作直接报空指针异常。
那么,有没有办法能够解决呢?
答案是肯定过的,在JDK1.5对关键字 volatile 进行优化,保证volatile修饰的变量不会被编译器进行优化排序

public class Person {
    private  volatile static Person mInstant = null;

    public  static Person getInstance(){
        if (mInstant == null) {
            synchronized (Person.class) {
                if (mInstant == null) {
                    mInstant = new Person();
                }
            }
        }
        return mInstant;
    }
    private Person() {
    }
}

这样就保证了消耗资源少的同时不会出错,当然你可以用静态内部类单例。

静态内部类单例

public class Person {
    
    public  static Person getInstance(){
        return PersonHolder.PERSON_HOLDER;
    }

    private static class PersonHolder{
        private static Person PERSON_HOLDER = new Person();
    }
    
    private Person() {
    }
}

这样写既可以保证不使用时不预创建浪费不必要的资源,又能保证在多线程时调用获取到同一个实例,那么实例化的时机怎么回事呢?我们来了解一下类的加载时机:

1.对类的new操作、静态变量的读写、静态方法的调用。
2.对子类进行初始化时,父类未初始化会进行初始化。
3.虚拟机启动时需要指定一个包含main方法的主类会对其进行初始化
4.对类进行的反射。
5.JDK1.7之后,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

.所以对类的静态内部属性进行引用,这时才会去初始化类的静态变量。那么此时多线程调用如何保证不会进行多次实例化呢?首先,在一个类加载器中,类只会初始化一次。其次,多线程初始化同一个类时,除了在进行初始化的线程,其余的都会阻塞等待,直到初始化完成。

枚举型单例

public enum PersonEnum {
    INSTANT;
}

就这么简单,枚举也算单例的一种,默认在初始化时进行实例化,同样的消耗资源,但枚举在源码中实现了线程安全,防反射和反序列化,感兴趣的自行百度,本文不再深究。

总结

本文介绍了五种单例模式,总的来说各有优缺点,不考虑资源浪费使用饿汉和枚举,考虑线程安全选择静态内部类单例,考虑安全选择枚举。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值