设计模式-单例模式(Singleton)详解

概述

  • 定义 : 保证一个类仅有一个实例, 并提供一个全局访问点
  • 又称单件模式
  • 类型 : 创建型

适用场景

  • 想确保任何情况下都绝对只有一个实例

优点

  • 在内存里只有一个实例, 减少了内存开销
  • 可以避免对资源的多重占用
  • 设置了全局访问点, 严格控制访问

缺点

  • 没有接口, 扩展困难

重点

  • 私有构造器
  • 线程安全问题
  • 延迟加载
  • 序列化和反序列化安全问题
  • 反射安全问题

分类

单例模式分为懒汉式和饿汉式, 懒汉式是采用了延迟初始化的方式进行创建对象的, 而饿汉式是在类加载的时候就进行初始化的, 下面分别对两种模式进行一些分析

懒汉式

1. 首先, 来看一个最简单的懒汉式单例代码实现 :

/**
 * 单例模式 : 懒汉模式
 *
 * @author 七夜雪
 * 2018/11/14 17:21
 */
public class SingletonLazyInit {
   
    private static SingletonLazyInit singleton;
	// 单例模式构造器必须是私有的,防止外部使用new关键字构造新对象
    private SingletonLazyInit() {
   
    }

    public static SingletonLazyInit getInstance(){
   
        if (null == singleton){
   
            singleton = new SingletonLazyInit();
        }
        return singleton;
    }

}

上面的代码很简单, 在单线程情况下也是没有问题的, 但是在多线程的情况下就可能会存在问题, 比如说同时又两个线程t1, t2同时调用SingletonLazyInit的getInstance方法, t1执行到singleton = new SingletonLazyInit();这一句但是还没执行完时, t2判断singleton仍然为空, 扔能进入if代码块中, 这是t1对象创建成功并返回, 然后t2线程再进行对象创建, 这时t1和t2就获取的就不是一个对象了


2. 对于上面这种情况, 最简单的一种解决方案, 就是对getInstance方法进行加锁, 增加Synchronized关键字, 对静态方法进行加锁, 锁定的是当前类的class对象, 加锁之后的代码如下:

/**
 * 单例模式 : 加锁单例
 *
 * @author 七夜雪
 * 2018/11/14 17:21
 */
public class SingletonSynchronized {
   
    private static SingletonSynchronized singleton;

    private SingletonSynchronized() {
   
    }

    // 写法一
    public static synchronized SingletonSynchronized getInstance(){
   
        if (null == singleton){
   
            singleton = new SingletonSynchronized();
        }
        return singleton;
    }

      // 写法二
//    public static synchronized SingletonSynchronized getInstance(){
   
//        synchronized (SingletonSynchronized.class){
   
//            if (null == singleton){
   
//                singleton = new SingletonSynchronized();
//            }
//        }
//        return singleton;
//    }

}
  • getInstance方法上进行加锁之后, 在多线程的情况下就能保证单例的正确性了, 但是每次调用getInstance方法都有进行获取锁和释放锁的操作, 对性能是由一定影响的
  • 不过synchronized的性能并没有想象中的那么差, 是因为在jdk1.6之后, 对synchronized进行了很多优化, 引入了偏向锁和轻量级锁的概念, 详细情况这里就不多说了, 想了解的可以参考这篇文章:https://blog.csdn.net/love905661433/article/details/82871531
  • 继续说回到设计模式, 对于这种情况, 引入了双重检查的单例模式
    3. 双重检查的单例模式代码如下 :
/**
 * 单例模式 : 双重检查
 * 防止并发情况下问题
 * @author 七夜雪
 * 2018/11/14 17:26
 */
public class SingletonDoubleCheck {
   
    private static SingletonDoubleCheck singleton;

    private SingletonDoubleCheck() {
   
    }

    public static SingletonDoubleCheck getInstance(){
   
        if (null == singleton){
   
        	// 加锁保证这个代码块只有一个线程能够执行
            synchronized (SingletonDoubleCheck.class){
   
            	// 避免在获取锁的过程中, 对象被其他线程创建, 所以再进行一次检查
                if (null == singleton) {
   
                    singleton = new SingletonDoubleCheck();
                }
            }
        }

        return singleton;
    }
}
  • 通过两次null == singleton判断, 既保证了反复加锁的问题, 又保证了多线程情况下单例模式能够正常工作
    看到这里是不是都绝对这个方案已经完美解决了并发安全性以及性能问题呢, 事实上这个代码仍然是由问题的, 在多线程情况下, 仍然存在风险, 为什么会存在风险呢? 这是因为在jvm中, 存在指令重排序现象, 下面我们具体来进行分析一下 :
  • singleton = new SingletonDoubleCheck();首先这行看起来只有一句, 但其实是分成了三条指令进行执行的 :
  1. 分配内存给这个对象
  2. 初始化对象
  3. 设置singleton指向刚刚分配的内存地址
  • 但是对于上面的情况, 2和3可能会被重排序, 执行顺序有可能是1->2->3, 也有可能是1->3->2, 因为jvm规范中只要求单线程情况下这种情况能够获得正确的结果
  • 如果执行顺序是1->3->2的顺序的话, 执行了第三步之后, 这个时候singleton就已经不为null了, 这是如果第二个线程进入的话, 判断发现singleton不为null, 直接返回的话, 就会出现问题, 因为这时singleton对象并没有初始化完成, 具体参见下图
    单线程情况 :
    在这里插入图片描述
    并发情况 :
    在这里插入图片描述
    可以看出在并发情况下, 线程1有可能会被访问到并没有初始化完成的对象

通过上面的分析可以知道, 上面的这种double check的单例模式仍然是存在问题, 那这个问题该如何解决呢?
4. 事实上解决上面的这个问题非常简单, 只需要修改一行代码 :

只需要对这一行声明private static SingletonDoubleCheck singleton;
增加一个volatile关键字即可 : private static volatile SingletonDoubleCheck singleton;
为何增加一个volatile就能解决这个问题呢, 简单来说就是volatile可以禁止2, 3两步进行指令重排序, 具体更多关于volatile的信息可以参考 : https://blog.csdn.net/love905661433/article/details/82833361

除了上面的volatile关键字之外, 还有第二种方式解决指令重排序造成的线程安全问题 :

/**
 * 基于静态内部类的单例模式
 *
 * @author 七夜雪
 * @create 2018-11-22 20:57
 */
public class StaticInnerClassSingleton {
   
    // 注意私有的构造方法
    private StaticInnerClassSingleton(){
   

    }

    public StaticInnerClassSingleton getInstance(){
   
        return InnerClass.instance;
    }

    private static class InnerClass{
   
        private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

}

为何使用这种静态内部类的方式就能解决指令重排序问题呢, 这是因为jvm在进行Class对象初始化的时候, 会增加Class对象的初始化锁, 所以哪个对象能够拿到静态内部类的初始化锁, 哪个对象就能完成对静态内部类的初始化, 所以即使存在指令重排序, 也不影响线程安全性问题, 如下图:

在这里插入图片描述
关于Class类的初始化问题, 这里提一下, 在发生下面几种情况下, 会对Class类进行初始化:

  • 实例化一个类,new一个类的实例对象
  • 访问类的静态变量
  • 调用类的静态方法
  • 通过反射调用类
  • 实例化类的子类
  • 被标位启动类的类

饿汉式

  1. 首先仍然是看下最简单的饿汉式单例模式写法:
/**
 * 单例模式 : 饿汉模式
 * @author 七夜雪
 * 2018/11/14 17:13
 */
public class Singleton {
   
    private final static Singleton instance = new Singleton();

    public static Singleton getInstance() {
   
        return instance;
    }

    private Singleton() {
   
    }
}
  • 上面这种饿汉式的写法很简单, 就是在Class类加载的时候, 完成instance对象的创建, 所以不存在任何线程安全问题
  • 唯一的问题就是, 不管这个单例对象有没有用到, 在类加载完成之后这个对象就已经被创建成功了, 会造成一定的内存浪费, 如果这个对象很小的话, 这种影响并不大, 也可以将初始化放在静态代码块中, 效果是一样的, 代码如下:
/**
 * 单例模式 : 静态代码块饿汉模式
 *
 * @author 七夜雪
 * 2018/11/14 17:13
 */
public class StaticHungrySingleton {
   
    private static StaticHungrySingleton instance;

    static {
   
        instance = new StaticHungrySingleton();
    }

    public static StaticHungrySingleton 
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值