设计模式之单例模式

本文详细探讨了单例模式的定义、使用场景以及多种实现方式(包括饿汉式、懒汉式及其改进版),涉及线程安全、双重校验锁、volatile和静态内部类,同时指出如何通过反射和序列化破坏单例模式,最后介绍了Enum的单例模式实现。
摘要由CSDN通过智能技术生成

目录

1. 定义

2. 使用场景

3. 实现方式

3.1 饿汉式

3.1.1 静态成员变量

3.1.2 静态代码块

3.2 懒汉式

3.2.1 线程不安全

3.2.2 线程安全

3.2.3 双重校验加锁

3.2.4 volatile防止指令重排

3.2.5 静态内部类

3.3 破坏单例模式

3.4 Enum实现单例模式

4. 参考资料

1. 定义

程序运行过程中,某类在内存中仅存在一个实例对象。

2. 使用场景

在程序中需要多次创建某类对象且使用对象目的又完全相同时,可以使用考虑使用单例模式来实现此类,避免在内存中频繁创建和销毁此类对象。

3. 实现方式

3.1 饿汉式

饿汉式:在类加载时就预先创建实例对象,在程序调用时就直接返回创建好的实例对象。

3.1.1 静态成员变量

public class Singleton {

    // 类加载时就会在内存中创建实例对象,如果不被调用,会造成内存浪费
    private static final Singleton SINGLETON = new Singleton();

    // 私有化构造函数,防止外界创建实例对象
    private Singleton() {

    }

    // 对外提供获取唯一实例对象的方法
    public Singleton getInstance() {
        return SINGLETON;
    }

}

通过静态成员变量实现单例模式,可能造成内存浪费,但不存多线程并发访问问题。

3.1.2 静态代码块

public class Singleton {

    // 声明实例对象
    private static Singleton singleton;

    // 类加载时执行静态代码块,初始化实例对象
    static {
        singleton = new Singleton();
    }

    // 私有化构造函数,防止外界创建实例对象
    private Singleton() {

    }

    // 对外提供获取唯一实例对象的方法
    public Singleton getInstance() {
        return singleton;
    }

}

通过静态代码块实现单例模式,可能造成内存浪费,但不存多线程并发访问问题。 

3.2 懒汉式

懒汉式:在需要使用实例对象时才创建实例对象。

3.2.1 线程不安全

public class Singleton {

    // 声明实例对象
    private static Singleton singleton;

    // 私有化构造函数,防止外界创建实例对象
    private Singleton() {

    }

    // 对外提供获取唯一实例对象的方法
    public Singleton getInstance() {
        // 为null时才创建实例对象;不为null时直接返回已创建好的实例对象
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }

}

通过普通懒汉式实现单例模式,虽然解决浪费内存问题,但是引入了多线程并发访问问题,多线程场景下无法保证只创建唯一实例对象。

3.2.2 线程安全

public class Singleton {

    // 声明实例对象
    private static Singleton singleton;

    // 私有化构造函数,防止外界创建实例对象
    private Singleton() {

    }

    // 对外提供获取唯一实例对象的方法
    public Singleton getInstance() {
        // 添加锁,保证线程安全
        synchronized (Singleton.class) {
            if (singleton == null) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }

}

上面代码虽然通过代码块锁保证了线程安全,但是存在性能问题,因为每次获取实例对象时都需要获取锁,并发访问时性能差。

3.2.3 双重校验加锁

在代码块锁外层添加判断,如果已创建好实例对象,就无需获取锁,而是直接返回实例对象

public class Singleton {

    // 声明实例对象
    private static Singleton singleton;

    // 私有化构造函数,防止外界创建实例对象
    private Singleton() {

    }

    // 对外提供获取唯一实例对象的方法
    public Singleton getInstance() {
        // 添加判断,实例对象已创建就无须获取锁,提高多线程并发访问性能
        if (singleton == null) {
            // 添加锁,保证线程安全
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}

3.2.4 volatile防止指令重排

指令重排:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。

创建一个对象,在JVM中会经过三步(以singleton对象为例):

1. 为singleton分配内存空间;

2. 初始化singleton对象;

3. 将singleton指向分配好的内存空间;

在上述步骤中,2和3可能会发生指令重排现象,导致创建对象的顺序变为1、3、2,当多个线程获取singleton对象时,线程A先执行了1、3步骤,然后线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。所以在3.3.2双重校验加锁的基础上,还需添加volatite关键字防止指令重排,代码如下:

public class Singleton {

    // 声明实例对象; volatile 防止指令重排
    private static volatile Singleton singleton;

    // 私有化构造函数,防止外界创建实例对象
    private Singleton() {

    }

    // 对外提供获取唯一实例对象的方法
    public Singleton getInstance() {
        // 添加判断,实例对象已创建就无须获取锁,提高多线程并发访问性能
        if (singleton == null) {
            // 添加锁,保证线程安全
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}

3.2.5 静态内部类

public class Singleton {

    // 私有化构造函数,防止外界创建实例对象
    private Singleton() {

    }

    // 外部类加载时,不会加载静态内部类
    private class SingletonHolder {
        // 外部类实例对象由静态内部类创建
        private static final Singleton SINGLETON = new Singleton();
    }

    // 对外提供获取唯一实例对象的方法
    public Singleton getInstance() {
        // 静态内部类被使用时才会被加载,从而创建静态成员变量
        return SingletonHolder.SINGLETON;
    }

}

通过静态内部类方式实现单例模式,在使用实例对象时才创建实例对象,避免浪费内存,而且也不会出现多线程并发访问问题。

3.3 破坏单例模式

3.1和3.2中所有单例模式实现方式,均可以被反射机制和序列化/反序列化破坏,让单例模式创建的实例对象不再唯一。以3.1.1静态成员变量实现单例模式为例,破坏单例模式代码如下所示:

public class Singleton implements Serializable {

    // 类加载时就会在内存中创建实例对象,如果不被调用,会造成内存浪费
    private static final Singleton SINGLETON = new Singleton();

    // 私有化构造函数,防止外界创建实例对象
    private Singleton() {

    }

    // 对外提供获取唯一实例对象的方法
    public Singleton getInstance() {
        return SINGLETON;
    }

    public static void main(String[] args) {
        destroySingletonByReflect();
        destroySingletonBySerialize();
    }

    // 通过反射破坏单例模式
    private static void destroySingletonByReflect() {
        try {
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton singleton2 = constructor.newInstance();
            System.out.println(singleton2); // com.zh.pattern.Singleton@16b98e56
            System.out.println(SINGLETON);  // com.zh.pattern.Singleton@7ef20235
        } catch (Exception e) {
            System.out.println("destroySingletonByReflect occur exception : " + e);
        }
    }

    // 通过序列化/反序列化破坏单例模式
    private static void destroySingletonBySerialize() {
        try {
            // 序列化
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SingletonFile"));
            oos.writeObject(SINGLETON);
            // 反序列化
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("SingletonFile"));
            Singleton singleton3 = (Singleton) ois.readObject();
            System.out.println(singleton3); // com.zh.pattern.Singleton@66cd51c3
            System.out.println(SINGLETON);  // com.zh.pattern.Singleton@7ef20235
        } catch (Exception e) {
            System.out.println("destroySingletonBySerialize occur exception : " + e);
        }
    }

}

3.4 Enum实现单例模式

(1)通过反射机制强制创建枚举对象时,Enum内部会校验枚举类型,防止反射创建多个对象。
(2)枚举对象序列化时会向文件写入枚举类型和枚举对象名称,反序列化时通过valueOf() 方法匹配枚举类型和枚举对象名称,找到内存中的唯一的实例对象,防止通过反序列化创建多个对象。

public enum Singleton {

    // 枚举类Singleton被加载时,通过无参构造函数Singleton()创建实例对象SINGLETON
    SINGLETON;

    private Singleton() {
        System.out.println("Constructor Singleton");
    }

}

class Test {

    public static void main(String[] args) {
        Singleton singleton1 = Singleton.SINGLETON;
        Singleton singleton2 = Singleton.SINGLETON;
        System.out.println(singleton1 == singleton2);
        // Constructor Singleton
        // true
    }

}

Enum实现单例模式,既是线程安全的,又可避免反射机制和序列化/反序列化破坏。

4. 参考资料

单例模式详解

我给面试官讲解了单例模式后,他对我竖起了大拇指!

秒懂设计模式之单例模式(Singleton Pattern)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值