设计模式(一):单例模式(上)

Published on 2024-02-26 20:13 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、单例模式的定义

单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:

Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

举个最常见的例子,Spring 中的 bean 默认都是单例模式,每个bean定义只生成一个对象实例,每次getBean请求获得的都是此实例

单例模式的通用类图如图:

image-20240225192325815

二、单例模式优缺点

1、单例模式的优点

● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。

● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

2、单例模式缺点

● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

● 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

● 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单 例”和业务逻辑融合在一个类中。

三、单例模式的四种实现方式

单例模式的实现需要考虑的最大的问题就是线程安全性问题,在多线程环境下,若实现方式不正确,有可能会创建多个对象;其次要考虑不要实现Cloneable接口,以免单例对象通过clone()方法被复制,当然这种情况基本上见不到,考虑最多的就是多线程环境下的线程安全性问题了。

单例模式作为面试中最高频的设计模式,它有四种实现方式:饿汉式、懒汉式、静态内部类、枚举方式。

1、饿汉式(静态常量)

饿汉式的英文名为 Eager Initialization,在该实现方式中,单例实例在类加载时就被创建,所以被称为“饿汉式”。这是一种最常见、最基础的写法,代码模板如下所示:

// 饿汉式(静态变量)
class Singleton{
    //构造器私有化
    private Singleton(){

    }
    //类的内部创建对象
    private final static Singleton singleton = new Singleton();

    //对外提供公共的,静态的方法
    public static Singleton getInstance(){

            return singleton;
    }
}

其具体写法如下:

1、构造器私有化(防止外部通过new + 构造器的方式创建对象实例)

2、类的内部创建对象(创建final,static的实例对象)

3、对外暴露一个公共的静态方法(通过该方法,返回该类唯一的对象实例)

这种写法比较简单,就是在类装载的时候就完成了实例化,避免了线程同步问题,但是更多情况下是没有办法使用这种方式的:比如实例化的时候需要依赖外部资源,只有在调用的时候才能装配完这些外部资源依赖,这时候就必须在getInstance方法中实例化。

当然,饿汉式还有静态代码块的写法,代码如下所示

//饿汉式 静态代码块
class Singleton{

    private Singleton(){

    }

    private final static Singleton singleton;

    static {
        singleton = new Singleton();
    }

    public static Singleton getInstance(){

        return singleton;
    }
}

这种方式和 **饿汉式(静态常量)**实现方式唯一的区别就是将实例化过程放到了静态代码块中,其它没有任何区别,它们都是线程安全的

2、懒汉式(同步代码块)

懒汉式的英文名为 Lazy Initialization ,在该实现方式中,单例实例在首次使用时才被创建,这种方式存在线程安全性问题。

懒汉式单例模式经典的实现方式是双重检查方式,也是个人最常用的一种实现方式。

//单例 双重检查机制
class Singleton{

    private Singleton(){
    }

    //一定要加volatile关键字
    private volatile static Singleton singleton;

    //提供一个静态的公共方法获取实例,加入双重检查
    public static Singleton getInstance(){
        if (singleton == null){
            synchronized (Singleton.class){
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

这种方式后续有单独篇章详细介绍。

3、静态内部类

静态内部类实现的单例模式写法如下

// 单例模式 - 静态内部类
class Singleton{

    private Singleton(){

    }

    private static class SingletonInstance {

        public static final Singleton INSTANCE = new Singleton();

    }

    public static Singleton getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

静态内部类的特点是,当Singleton类进行类加载的时候,静态内部类是不会被加载的

当调用Singleton类的 getInstance() 方法,用到了 SingletonInstance 的静态变量的时候,会导致静态内部类SingletonInstance 进行类加载,当然类加载的过程中,线程是安全的,所以这种写法不会出现线程安全问题

这种方式采用类加载的机制来保证初始化实例时只有一个线程, 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的 ,避免了线程不安全,利用静态内部类特点实现延迟加载,效率也较高,所以这种方式也是推荐使用的

4、枚举方式

//单例模式 - 枚举方式
enum Singleton{

    INSTANCE; //属性

    public void method(){
        System.out.println("实例方法的打印");
    }
}

public class EnumDemo {
    public static void main(String[] args) {

        Singleton instance = Singleton.INSTANCE;
        Singleton instance1 = Singleton.INSTANCE;
        System.out.println(instance == instance1);
        instance.method();
    }
}

这借助 JDK1.5 中添加的枚举来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,在实际开发中,同样推荐使用这种方式。

四、分析懒汉式的写法

上面的懒汉式的写法是最终模式的写法,它的初始模式写法如下

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

上述代码很明显有线程安全性问题,if(s==null)的操作在多线程环境下并不安全,这将会导致创建多个实例。

1、第一阶段:使用同步方法

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

这种方式能解决问题,但是每次进入方法都要先判断锁,效率很低下。

2、第二阶段:使用同步代码块

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

这种方式每次调用getInstance方法仍然会判断锁,事实上没有改变效率问题。

3、第三阶段:双重检查方式

我们可以使用另外一种方式,达到只判断一次锁,并且实现同步的目的:

class Singleton{
    private static Singleton s=null;
    private Singleton(){
        
    }
    public static Singleton getInstance()
    {
        if(s == null){//和上面的相比只是多增加了一次判断
            synchronized(Singleton.class){
                if(s==null)
                    s=new Singleton();
                return s;
            }
        }
    }
}

观察代码可以发现和上面的代码相比,只是增加了一次判断而已,但是,这一次判断却解决了效率问题。

假设我们现在并没有创建单例对象,即s==null,那么我们调用getInstance方法的时候,会进入if块,然后进入同步代码块,此时,别的线程如果想要创建Single实例,就必须获取锁;等当前线程创建完实例对象,释放锁之后,假设正巧有几个线程已经进入了if块中,它们会拿到锁,进入同步代码块,但是由于进行了判空操作,所以不会创建Single实例,而是直接返回已经创建好的Single实例。如果有多个其他线程进入了if块,当它们依次进入同步代码块的时候,同理也不会创建新的Single实例。而没有进入if块的线程,判空操作之后不满足条件,进不了if块,而直接执行了下一条语句return s;其后的线程调用getInstance方法时,只会判断一次s==null,不满足条件直接返回Single单例s,这样就大大提高了了执行效率。

总结下,在代码

if(s==null){
    synchronized(Singleton.class){
        if(s==null){
            s=new Singleton();
        }
        return s;
    }
}
return s;

中,第一行代码是第一次判空操作,目的是提高效率;第二行代码是同步代码块的入口,目的是保证线程安全;第三行代码进行第二次判空操作是为了保证单例对象的唯一性。

这似乎已经完美了。。。但是,真的是这样吗?这段代码,其实还可能出现空指针问题

vvO6n

4、第四阶段:volatile关键字

面试的时候,面试官可能会问,为什么单例模式要使用volatile关键字?其实,这句话需要翻译一下,翻译过来的完整问法是:在懒汉式单例模式下,私有变量为什么一定要加volatile关键字?

答:从结果上来看,在懒汉式单例模式下,如果不使用volatile关键字修饰私有变量,那有可能会出现使用拿到的单例对象的时候,出现空指针异常,出现这个问题的原因就是出现了指令重排序,解决问题的方式就是使用volatile关键字修饰私有变量,禁止指令重排序。

问题分析

第一步,先回顾下创建对象的过程:

1)分配内存地址 M

2)在内存 M 上初始化Singleton 对象

3)将M的地址赋值给 instance 对象

真正执行的时候,可能会变成这样:

1)分配内存地址 M

2)将M的地址赋值给instance变量

3)在内存M上初始化 Singleton 对象

这是由于处理器执行的时候发生了指令重排序。

指令重排序是一种优化技术,它可以改变指令的执行顺序,以提高程序的性能和执行效率。指令重排序的目标是在不改变单线程程序的语义和最终结果的前提下,充分利用处理器的执行能力和资源。

第二步,重新回顾下双重检查方式的代码

image-20240226135533696

为什么双重检查方式的单例模式私有变量不加volatile关键字修饰就有可能出现空指针异常?

因为在 ③ 处,在指令重排序的作用下,可能对象还没初始化完成就释放了锁,导致其它线程拿到锁进入了同步代码块,在 ② 处判定s是否为空,因为这时候s已经赋值完成,所以不为空,然后就走了 ④ ,直接返回了对象,这时候返回的对象实际上还没有初始化完成,直接使用访问其成员变量就可能出现空指针异常。

单例模式空指针问题

最终代码

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

参考文档:

https://developer.aliyun.com/article/780982?spm=a2c6h.13262185.profile.25.1d934622RdoUX1

https://developer.aliyun.com/article/780983?spm=a2c6h.13262185.profile.24.1d934622RdoUX1

https://developer.aliyun.com/article/780984?spm=a2c6h.13262185.profile.23.1d934622RdoUX1

https://www.51cto.com/article/709935.html



END.


#设计模式
目录