单例模式的实现与细节

一、单例模式概览

在《设计模式之禅》中提到了单例模式的定义如下:Ensure a class has only one instance, and provide a global point of access to it. (确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

在这里插入图片描述

Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自动实例化的(在Singleton中自己使用new Singleton() )。

细心留意会发现,单例模式在我们的日常开发中并不少见。例如:

  1. 数据库的连接池不会反复创建

  2. spring中一个单例模式bean的生成和使用

  3. 在我们平常的代码中需要设置全局的一些属性保存

    可见单例模式虽然简单,但却有着较广的使用面。同时单例模式也有一些细节值得我们注意,下面我们通过7种单例模式的写法来一一探讨这些细节。

二、7种单例模式的实现

1. 懒汉式,线程不安全

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

这段代码就是最基本的懒汉式单例模式实现。然而在多线程的环境下,会有多个请求同时调用getInstance( )方法,从而创建多个实例,造成资源浪费,也违背了单例的原则。

2. 懒汉式,线程安全

 public static sychronized Singleton getInstance(){
        if(instance == null){
            return new Singleton();
        }
        return instace
    }

为了解决上面的问题,最简单的方法就是将整个 getInstance() 方法设为同步(synchronized)。虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

3. 双重校验锁

public static Singleton getSingleton() {
    if(instance == null){ 						 //Single Checked
        sychronized(Singleton.class){
            if(instance == null){				 //Single Checked
                instance = new Singleton()
            }
        }
    }
}

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。通常称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

为了解决这个问题,我们需要将instance变量声明成volatile。

public class Singleton {
    private volatile static Singleton instance; //声明成 volatile
    private Singleton (){}

    public static Singleton getSingleton() {
        if (instance == null) {                         
            synchronized (Singleton.class) {
                if (instance == null) {       
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这里使用volatile能保证多线程状态不报错的主要原因是:volatile禁止指令重排序优化的特性。在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。

到这里,我们就写出了一个相对完美的懒汉式单例,不过我们也可以使用更多其他的实现方式。

4. 饿汉式 static final field

public class Singleton{
    // 类加载时就初始化
    private static final Singleton instance = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return instance;
    }
}

这种写法将单例的实例声明成final和static,在第一次加载类到内存中就会被初始化,是线程安全的。但因为它不是懒加载的,如果我们在后续的编程中并没有使用到它就会存在资源浪费。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

5. 静态内部类

public class Singleton{
    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton(){}
    
    public static final Singleton getInstace(){
        return SingletonHolder.INSTANCE;
    } 
}

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷。

6. CAS 线程安全

public class Singleton{
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
   
    private static Singleton instance;
    
    public static final Singleton getInstance(){
        for(;;){
            Singleton instance = INSTANCE.get();
            if(instance !=null){
                INSTANCE.compareAndSet(null,new Singleton());
            }
            return INSTANCE.get();
        }
        
    }
    
}

java并发库提供了很多原子类来支持并发访问的数据安全性;AtomicIntegerAtomicBooleanAtomicLongAtomicReference。AtomicReference 可以封装引用一个实例,支持并发访问如上的单例方式就是使用了这样的一个特点。

使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。

当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。

7. 枚举 Enum

public enum Singleton{
    INSTANCE;
}

创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。

以上就是单例模式实现的七种方式。但严格来说前两种都存在或多或少的问题,在多线程环境下应该尽量使用后面五种方法。

三、单例模式的优缺点及使用场景总结

由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,单例模式的优势就非常明显。同时,单例模式还可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。除此之外,单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

单例模式的缺点也很明显,那就是难以扩展,除了修改代码基本没有第二种途径可以实现。

单例模式的应用场景如下:

  • 要求生成唯一的序列号
  • 在整个项目中需要一个共享访问点或共享数据
  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库资源等
  • 需要定义大量的静态常量和静态方法(如工具类)的环境

以上,就是关于单例模式的一些基本知识。如何巧妙的运用这些设计模式到我们的实际代码编写中,仍然需要我们不断的学习和感悟。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值