23种设计模式——单例模式

单例模式

单例模式概述

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这一实例

在这里插入图片描述

单例模式类型

首先看一下原型模式:

@Data
public class Single {
    private String singleName;


    public static void main(String[] args) {
        Single single1 = new Single();
        Single single2 = new Single();
        single1.setSingleName("");
        System.out.println(single1.singleName);
        System.out.println(single2.singleName);
    }
}

测试结果:

single1
null

懒汉式

真正需要使用对象时才去创建该单例类对象

/**
 * 懒汉式单例
 */
@Data
public class LazySingle {
    private String singleName;
    private static LazySingle single=null;

    public static LazySingle getInstance(){
        if(single==null){
            single=new LazySingle();
        }
        return single;
    }

    public static void main(String[] args) {
        LazySingle single1=LazySingle.getInstance();
        LazySingle single2=LazySingle.getInstance();
        single1.setSingleName("懒汉式");
        System.out.println(single1.singleName);
        System.out.println(single2.singleName);
    }
}

测试结果

懒汉式
懒汉式

解决懒汉式的线程安全问题

由代码可以看出,线程是不安全的,多线程情况下不能保证是单例的,解决方案肯定是加锁,但加锁会导致性能低下,所以解决方案应该兼顾性能和安全实现

解决方案为: Double Check(双重校验) + Lock(加锁)

public static LazySingle getInstance() {
    if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
        synchronized(LazySingle.class) { // 线程A或线程B获得该锁进行初始化
            if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                singleton = new LazySingle();
            }
        }
    }
    return singleton;
}

但在JVM运行过程中会有一个问题:

指令重排

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

JVM创建一个对象会经过3步:

  1. 为对象分配内存空间
  2. 初始化对象
  3. 将对象指向分配好的内存空间

在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。

解决方案:使用volatile关键字修饰

  1. 可以保证其指令执行的顺序与代码顺序一致,不会发生顺序变换

  2. 可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

最终解决方案如下:

/**
 * 懒汉式单例
 */
@Data
public class LazySingle {
    private String singleName;
    private  static volatile LazySingle single=null;

    public static LazySingle getInstance(){
        if(single==null){
            synchronized (LazySingle.class){
                if(single == null){
                    single=new LazySingle();
                }
            }
        }
        return single;
    }
}

饿汉式

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的

/**
 * 饿汉式单例
 */
@Data
public class HungrySingle {
    private String singleName;
    private  static final HungrySingle singleton=new HungrySingle();
    public static HungrySingle getInstance() {
        return singleton;
    }

    public static void main(String[] args) {
        HungrySingle single1 = HungrySingle.getInstance();
        HungrySingle single2 = HungrySingle.getInstance();
        single1.setSingleName("饿汉式");
        System.out.println(single1.singleName);
        System.out.println(single2.singleName);
    }
}

测试结果

饿汉式
饿汉式

破坏单例的情况

java的反射和序列化可以破坏单例模式(饿汉式和懒汉式)

  1. 使用反射破坏单例模式(演示饿汉式)
 try {
            //获取类的显式构造器
            Constructor<HungrySingle> constructor = HungrySingle.class.getDeclaredConstructor();
            // 可访问私有构造器
            constructor.setAccessible(true);
            HungrySingle singleton1 = constructor.newInstance();
            HungrySingle singleton2 = constructor.newInstance();
            System.out.println(singleton1==singleton2);   //false
        } catch (Exception e) {
            e.printStackTrace();
        }
  1. 使用序列化与反序列化破坏单例模式(演示懒汉式)
 try {
            File file = new File("Singleton.txt");
            //创建输出流
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
            //将单例对象写到文件中  序列化
            oos.writeObject(LazySingle.getInstance());
            //从文件读取单例对象
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
            //反序列化得到对象lazySingle
            LazySingle lazySingle=(LazySingle)ois.readObject();
            System.out.println(lazySingle==LazySingle.getInstance()); //false
        } catch (Exception e) {
            e.printStackTrace();
        }

枚举实现单例模式

public enum Sex {
    MALE,FEMALE;


    public static void main(String[] args) {
        Sex male1 = Sex.MALE;
        Sex male2 = Sex.MALE;
        System.out.println(male1==male2);//true

    }
}

枚举的优势:

  1. 代码简洁
  2. 不需要额外操作保证线程安全和对象单一性
  3. 反射、序列化不能破坏枚举类的单例模式
    • 枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
    • 在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。

总结

  1. 单例模式常见实现方式:饿汉式懒汉式

  2. 懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

  3. 饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

  4. 懒汉式与饿汉式的选择:

    • 内存要求非常高,使用懒汉式写法,可以在特定时候才创建该对象;
    • 内存要求不高,使用饿汉式写法,因为简单不易出错,且没有任何线程安全和性能问题
  5. 最佳实现方式:枚举, 其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

远离bug,珍爱头发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值