c++ 单例模式_总结单例设计模式的实现和源码中的应用

重点:

1.模式定义/应用场景/类图分析

2.字节码知识/字节码指令重排

3.类加载机制

4.JVM序列化机制

5.单例模式在Spring框架 & JDK源码中的应用

模式定义:保证一个类只有一个实例,并且只提供一个全局访问点

使用场景:重量级的对象,不需要多个实例,如线程池,数据库连接池

a477f85929bb67cca2bfca5eb6f4d7a6.png

单例模式UML类图

1. 懒汉模式:延迟加载,只有真正使用的时候,才开始实例化

1)线程安全问题

2)double check 加锁优化

3)JIT编译器,CPU有可能对指令进行重排,导致使用到尚未初始化的实例,可以通过添加volatile关键字修饰解决。即volatile修饰的字段能够防止指令重排。

单线程环境下的实现:

public class LazyInstance {    public static void main(String[] args) {        LazySingleton lazySingleton1 = LazySingleton.getInstance();        LazySingleton lazySingleton2 = LazySingleton.getInstance();        System.out.println(lazySingleton2 == lazySingleton1);    }}class LazySingleton {    private static LazySingleton instance;    private LazySingleton() {    }    public static LazySingleton getInstance() {        if (instance == null) {            instance = new LazySingleton();        }        return instance;    }}
2acd77c710b191c740f7f2b0a4aab45d.png

多线程环境下全局访问方法getInstance()不加锁:

public class ConcurrentLazyInstance {    public static void main(String[] args) {        new Thread(() -> {            LazySingleton instance = LazySingleton.getInstance();            System.out.println("Thread1instance = " + instance);        }).start();        new Thread(() -> {            LazySingleton instance = LazySingleton.getInstance();            System.out.println("Thread2instance = " + instance);        }).start();    }}class LazySingleton {    private static LazySingleton instance;    private LazySingleton() {    }    public static LazySingleton getInstance() {        if (instance == null) {            instance = new LazySingleton();        }        return instance;    }}
24701df7f83758e636cc22bca8bd244c.png

破坏了单例的定义,产生了两个对象

多线程环境下全局访问方法getInstance()加锁:

public class ConcurrentLazyInstance {    public static void main(String[] args) {        new Thread(() -> {            LazySingleton instance = LazySingleton.getInstance();            System.out.println("Thread1instance = " + instance);        }).start();        new Thread(() -> {            LazySingleton instance = LazySingleton.getInstance();            System.out.println("Thread2instance = " + instance);        }).start();    }}class LazySingleton {    private static LazySingleton instance;    private LazySingleton() {    }    public synchronized static LazySingleton getInstance() {        if (instance == null) {            instance = new LazySingleton();        }        return instance;    }}
2975eca5a033d0e0b04ef4804de960ef.png

通过synchronize锁住方法,保证单例,但性能会有所下降

为了提高性能,getInstance()方法我们采用双重校验锁

public static LazySingleton getInstance() {   //先判断对象是否已经实例过,没有实例化过才进入加锁代码  if (instance == null) {     //类对象加锁    synchronized (LazySingleton.class) {       if (instance == null) {        instance = new LazySingleton();      }     }   }  return instance;  }}

另外,需要注意 instance 采用 volatile 关键字修饰也是很有必要。

instance 采用 volatile 关键字修饰也是很有必要的, instance = new LazySingleton(); 这段代码其实是分

为三步执行:

1). 为 instance 分配内存空间

2). 初始化 instance

3). 将 instance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在

多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用

getInstance() 后发现 instance 不为空,因此返回 instance,但此时 instance 还未被

初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

  1. 饿汉式:JVM类加载的初始化阶段就完成了实例化的初始化。本质上借助类加载机制,保证实例的唯一性。

类加载的过程:

1)加载:二进制数据加载到内存,生成对应的Class数据结构

2)连接:a.验证,b.准备(给类的静态成员变量赋默认值),c.解析

3)初始化:给类的静态变量赋初值

单线程环境下的实现饿汉式:

public class HungrySingletonTest {    public static void main(String[] args) {        HungrySingleton lazySingleton1 = HungrySingleton.getInstance();        HungrySingleton lazySingleton2 = HungrySingleton.getInstance();        System.out.println(lazySingleton2 == lazySingleton1);    }}class HungrySingleton {    private static HungrySingleton instance = new HungrySingleton();    private HungrySingleton() {    }    public static HungrySingleton getInstance() {        return instance;    }}
  1. 静态内部类

1)本质上是利用类的加载机制保证线程线程安全

2)当在实际使用的时候才会触发类的初始化。所以也是懒加载的一种形式

class InnerClassSingleton {    private static class InnerClassHolder {        private static InnerClassSingleton instance = new InnerClassSingleton().getInstance();    }    private InnerClassSingleton() {    }    public static InnerClassSingleton getInstance() {        return InnerClassHolder.instance;    }}
  1. 反射攻击

在饿汉模式和静态内部类初始化情况下,反射实例化和单例实例化不是一个对象

public class HungrySingletonTest {    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {        //反射实例化和单例实例化不是一个对象        //如何防止?在饿汉模式和静态内部类的初始化构造函数中判断类的实例是否为null,不等于null则抛出异常        Constructor declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();        declaredConstructor.setAccessible(true);        InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();        InnerClassSingleton instance3 = InnerClassSingleton.getInstance();        System.out.println(instance3==innerClassSingleton);    }}class HungrySingleton {    private static HungrySingleton instance = new HungrySingleton();    private HungrySingleton() {    }    public static HungrySingleton getInstance() {        return instance;    }}class InnerClassSingleton {    private static class InnerClassHolder {        private static InnerClassSingleton instance = new InnerClassSingleton();    }     private InnerClassSingleton() {        if(InnerClassHolder.instance != null){          //在饿汉模式和静态内部类的初始化构造函数中判断类的实例是否为null,不等于null则抛出异常            throw new RuntimeException("单例不允许多个实例");        }    }    public static InnerClassSingleton getInstance() {        return InnerClassHolder.instance;    }}

如何防止?答:在饿汉模式和静态内部类的初始化构造函数中判断类的实例是否为null,不等于null则抛出异常

  1. 枚举类型

1) 天然不支持反射创建对应实例,且有自己的反序列化机制

2) 利用类加载机制保证线程安全

public enum EnumsSingleton {    INSTANCE;    public void print() {        System.out.println("this.hashCode() = " + this.hashCode());    }}class EnumTest {    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {        /**         *         EnumsSingleton instance = EnumsSingleton.INSTANCE;         EnumsSingleton instance1 = EnumsSingleton.INSTANCE;         System.out.println(instance==instance1);         */        Constructor declaredConstructor = EnumsSingleton.class.getDeclaredConstructor(String.class, int.class);        declaredConstructor.setAccessible(true);        EnumsSingleton enumsSingleton = declaredConstructor.newInstance("INSTANCE", 0);    }}
f297b64da2a2c2645a527c261d0d0957.png

enum天然不支持反射创建对应实例

  1. 通过InnerClassSingleton将instance4实例序列化到磁盘上,在项目的根目录,再从磁盘反序列化到内存中,读出对象,比较放进去的对象和我们读出来的对象是不是一个对象
public class HungrySingletonTest {    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException,       InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {        //instance4实例序列化到磁盘上,在项目的根目录        InnerClassSingleton instance4 = InnerClassSingleton.getInstance();        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("testSerializable"));        oos.writeObject(instance4);        oos.close();        //从磁盘反序列化到内存中,读出对象,比较放进去的对象和我们读出来的对象是不是一个对象        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));        InnerClassSingleton object = (InnerClassSingleton) ois.readObject();        System.out.println(instance4 == object);    }}class HungrySingleton {    private static HungrySingleton instance = new HungrySingleton();    private HungrySingleton() {    }    public static HungrySingleton getInstance() {        return instance;    }}class InnerClassSingleton implements Serializable {    private static class InnerClassHolder {        private static InnerClassSingleton instance = new InnerClassSingleton();    }    private InnerClassSingleton() {        if (InnerClassHolder.instance != null) {            throw new RuntimeException("单例不允许多个实例");        }    }    public static InnerClassSingleton getInstance() {        return InnerClassHolder.instance;    }}
b8c7c5d29df6d591053b520975e12b8f.png

这是JVM序列化的一个机制,在源码中给出了解决方案:在InnerClassSingleton类实现readResolve()方法

   Object readResolve() throws ObjectStreamException {        return InnerClassHolder.instance;    }

为了在序列化反序列化的时候保证数据是一致的,我们可以在类InnerClassSingleton中加入版本号:

static final long serialVersionUID = 42L;

能够保证写入的对象和读出的对象是同一个对象

ac764a28cbb6868e3afac76a445b54b0.png

类似的,枚举是天然的保证序列化和反序列化为同一个对象

  1. 单例在源码中应用举例:

Runtime--->有饿汉模式

Currency ---->有享元模式(注意他的反序列化也是用readResolve)

DefaultSingletonBeanRegistry---->有双重校验锁

ReactiveAdapterRegistry---->有双重校验锁

ProxyFactoryBean

TomcatURLStreamHandlerFactory(Tomcat中)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值