单例模式 - 双重检查锁定(Double-Checked Locking)

一、基本介绍

1.1 概念

单例模式,即单个实例的模式,开发中非常常见,比如全局对象管理、工具类等基本都会使用单例模式。

1.2 优缺点

优点:

  • 节约资源:不用频繁创建对象,减少了系统开销。
  • 方便访问:提供全局统一访问点,便于调用。

缺点:

  • 由于单例对象一般都是长生命周期对象,使用不当很有可能导致内存泄漏(单例对象持有短生命周期对象导致其无法被回收)。

二、实现

1.1 饿汉式

听这个名字可能一头雾水,可以这样想:类似于一个"饥饿"的人,在程序启动时就要马上吃饭(创建实例),不管后续是否会真正需要使用这个实例
实现如下:

public class Singleton {
    // 类加载的时候就new一个对象
    private static Singleton INSTANCE = new Singleton();
      
    // 私有构造器,防止外部创建实例
    private Singleton() {}
        
    // 全局统一访问点
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

饿汉式是最简单的一种单例实现方式,但是同时也带来了资源浪费的问题,上面也说了,不管后续有没有使用到该类实例,实例都已经被创建了,这显然是不太优雅的。

1.2 懒汉式

由于饿汉式不够优雅,懒汉式应运而生,这个名字更好理解,可以想象成这个人很"懒",只有在饿得不行的时候才吃饭(创建对象)。
实现如下:

public class Singleton {
    
    private static Singleton INSTANCE;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
      	if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
      	return INSTANCE;
    }
}

这样就优雅不少了,只有在真正需要的时候,这个单例对象实例才会被创建。
但是,优雅仅限于单线程的情况,在多线程环境下,上面方法并不安全。
比如,此时有AB两线程同时进入getInstance()方法,并且此时INSTANCE实例之前还未被创建过,就会出现如下情况:

  1. 线程A首先判断INSTANCEnull,然后开始创建新的实例。
  2. 在线程A还未完成实例化之前,线程B也判断INSTANCEnull,然后也开始创建新的实例。
  3. 最终,线程A和线程B分别创建了各自的实例,导致系统中存在多个实例,违背了单例模式的初衷。

1.2.1 懒汉式优化①

不就是多线程嘛,加锁!由此,你得出了第一种优化方案,给getInstance()方法加锁

public class Singleton { 
    
    private static Singleton INSTANCE;
    
    private Singleton() {}
    
    // 保证每次只有一个线程进入getInstance()方法
    public static synchronized Singleton getInstance() {
      	if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
      	return INSTANCE;
    }
}

这样不就好了吗,即实现了懒加载,又实现了线程安全。
但是,性能呢?上面这个锁明显属于粗粒度锁,以后每次访问都需要先获取锁才能进入getInstance()方法,显然是一笔巨大的性能开销。

1.2.2 懒汉式优化②

思考之后,你得出了第二种优化方案,即只对方法中的创建实例的局部代码加锁,也就是降低锁的粒度:

public class Singleton { 
    
    private static Singleton INSTANCE;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) { // --> 位置1
                INSTANCE = new Singleton();  // --> 位置2
            }
        }
        return INSTANCE;
    }
}

这样的话,每次线程进来会先获取锁,然后只有一个线程去执行同步代码块,创建INSTANCE实例。
但是,上面这样写对吗?不妨举个例看一下:

  1. 线程AB同时进入getInstance()方法,由于是第一次进入,INSTANCE还未实例化,二者都到了位置1
  2. 假设线程A抢到了锁,到达位置2,创建完对象之后退出同步代码块释放锁。
  3. 接下来线程B拿到锁,到达位置2,此时线程B并不知道A已经创建了实例,仍会创建另一个对象实例。
  4. 最终线程A和线程B分别创建了各自的实例,导致系统中存在多个实例,完了,又搞回去了。

1.2.3 懒汉式优化③

不过这次简单,再加个判空就行了嘛,于是有了双重检查锁定(double-checked locking)

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

nice!大功告成!欸等等,怎么第一个if那里有个警告?点开灯泡看一下:

哦?IDE 已经发现我们写出了一段双重检查锁定的代码,并贴心的给出了提示信息。
按照提示信息,我们在INSTANCE变量声明时加上volatile

public class Singleton { 
    
    private static volatile Singleton INSTANCE;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

警告消失!至此,终于是完成了单例模式的懒汉式实现。

最后留下一个问题,就是volatile在这里究竟是起到了防止指令重排,还是可见性的效果?还是两者都有?网络上对此也是各种说法,我个人更倾向于可见性一点,欢迎留言一起讨论!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值