设计模式之单例模式

前言

提一个问题,大家知道spring容器管理的bean,默认是单例的么,如果是,那为什么用到单例呢, 有什么好处呢。至于spring是如何实现bean的单例我们到spring的章节再细聊,我们今天只聊聊单例模式。

定义

简单来说,就是保证一个系统只有一个实例,只提供了一个对外访问点

特点

  • 只有一个实例。
  • 自我实例化。
  • 提供全局访问点。

优点

由于单例模式只会生成一个实例,自然能够节约系统资源,减少性能开销

缺点

没有接口,也不能继承,违背了单一职责(一个类应该只关心内部的逻辑,而不应该关心外部的实例化)

代码实现

懒汉单例模式

懒汉单例,顾名思义,就是他是个很懒的类,你不主动调用他时,他是不会主动实例化的,我们来看代码

class SingleExample01 {

    private static SingleExample01 singleExample;

    // 避免通过构造方法创建对象
    private SingleExample01() {
    }

    public static SingleExample01 getInstance() {
        if (singleExample == null){
            singleExample = new SingleExample01();
        }
        return singleExample;
    }
}

上述代码是线程不安全的,在多线程的情况下,无法保证对象的单例,所以严格来说他不算真正的单例模式

其实我们要想保证线程安全,最简单的方法就是在获取单例方法的上面加synchronized锁,保证每次进入这个方法的只有一个线程

class SingleExample001 {

    private static SingleExample01 singleExample;

    // 避免通过构造方法创建对象
    private SingleExample01() {
    }

    public static synchronized SingleExample01 getInstance() {
        if (singleExample == null){
            singleExample = new SingleExample01();
        }
        return singleExample;
    }
}

优点

当对象需要的时候才会被实例化,避免产生垃圾对象,造成内存浪费

缺点

如果不加锁,这种单例模式是线程不安全的,在多线程的情况下,很难保证单例;但是加了锁,就会导致效率低下

饿汉式单例

饿汉单例,顾名思义,就是他很饥渴,不管其他需不需要,我先实例化好,等调用方需要的时候,就不需要再实例化,直接返回给调用方

class SingleExample02 {
    // JVM保证线程安全
    private static final SingleExample02 singleExample = new SingleExample02();

    // 避免通过构造方法创建对象
    private SingleExample02() {
    }

    public static SingleExample02 getInstance() {
        return singleExample;
    }
}

饿汉式单例天生就是线程安全的,因为被static修饰,在类加载的时候,对象就会被实例化出来,并且只会被实例化一次

优点

不需要加锁,执行效率较高

缺点

可能会创建垃圾对象,造成内存浪费

双检锁(双重校验锁)

这种单例其实也是一种饿汉式单例,不过他保证了线程的安全性。直接上代码吧

class SingleExample03 {

    private static volatile SingleExample03 singleExample;

    // 避免通过构造方法创建对象
    private SingleExample03() {
    }

    public static SingleExample03 getInstance() {
        if (singleExample == null){
            synchronized (SingleExample03.class){
                if (singleExample == null){
                    singleExample = new SingleExample03();
                }
            }
        }
        return singleExample;
    }
}

我们看到上述代码,有没有两个疑问:

  • 为啥判断了两次对象为null的情况
  • 为啥给对象变量前加volatile关键字

我们一个个来解答

为啥判断了两次对象为null的情况

  • 第一个null判断是为了价格前置条件,避免大量线程去争夺锁资源,造成系统不必要的资源浪费
  • 第二个null的判断可以这样理解,我们有三个线程A,B,C同时调用getInstance方法,这时候A,b,c都通过了第一个null判断,然后在锁的争夺中,A抢到了锁,B,C等待,A实例化了对象释放了锁。接着B争夺到了锁,如果没有null判断,B就会直接实例化对象,这样就无法保证对象的单例了。

那么只做了两次非空判断就一定能够保证对象的单例了么

为啥给对象变量前加volatile关键字

我们在来说说volatile关键字

首先我们实例化对象的操作并不是原子性的,需要经历三个步骤

  1. 在堆中开辟对象所需空间,分配地址
  2. 根据类加载的初始化顺序进行初始化
  3. 将内存地址返回给栈中的引用变量

由于CPU的优化程序,可能会对我们的指令进行重排,将本来123的执行顺序变为132,这样对象的变量就会指向对应的内存地址,而这时候对象还没有真正的进行初始化。

我们都知道volatile有两个特性

  • 禁止指令重排
  • 保证可见性

我们假设一个场景,有两个线程A,B,当A进入获取到了锁,根据上面说的重排序问题,A走到了1,3两步,还没有执行2步;这时候B线程进来了,获取单例的时候,发现对象变量不为空,就直接返回了这个还没有初始化的对象。我们在使用对象的时候就会出现问题,这也就是利用volatile禁止指令重排序的原因

静态内部类

除了双检锁实现单例的懒加载线程安全实现,静态内部类也可以做到,并且方法更简单。内部类只有被调用使用的时候才会被类加载器加载

class SingleExample04 {

    private static class  SingleExampleHolder{
        //静态初始化器,由JVM来保证线程安全
        private static final SingleExample04 singleExample = new SingleExample04();
    }

    public static SingleExample04 getInstance() {
        return SingleExampleHolder.singleExample;
    }
}

枚举单例模式

enum SingleExample05{

    SINGLE_EXAMPLE_05;
}

是不是非常简单,非常简洁,而且无偿地提供了序列化机制。

TODO: 至于枚举的工作原理,目前我也不是很了解,等之后了解了再出一篇文章

注意点

  1. 构造方法私有化
  2. 自我实例化
  3. 提供一个公开方法让外部获取类的单例实例

如何破坏单例呢

其实这个问题可以换个问法

除了用new关键字创建对象,还有哪些方法可以创建对象

  • 反射
  • 序列化
  • 克隆
    具体如何实现,大家可以自行研究下 ^ v ^
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值