• 单例模式


    一、单例模式

    1、单例模式(Singleton Pattern):指确保一个类在任何情况下都绝对只有一个实例,私有化其所有构造方法,并提供一个全局访问点。(属于创建型模式)

    2、适用场景

    确保任何情况下都绝对只有一个实例(如ServletContext、ServletConfig、ApplicationContext、DBPool)。

    3、常见写法

    • 饿汉式单例
    • 懒汉式单例
    • 注册式单例
    • ThreadLocal单例

    4、优点

    • 在内存中只有一个实例,减少了内存开销
    • 可以避免对资源的多重占用
    • 设置全局访问点,严格控制访问

    5、缺点

    • 没有接口,扩展困难
    • 如果要扩展单例对象,只有修改代码,没有其他途径

    二、饿汉式单例

    1、饿汉式单例:指在单例类首次加载时就创建实例。

    2、缺点:如果这个对象实例从头到尾都没有被使用过的话,会浪费内存空间。

    例子:下面两种方式都可以。 

    ① 常规:

    ② 静态代码块:

    提问:这两种饿汉式为什么成员变量要加final关键字?

    防止该实例被篡改。如果不加final的话,该实例有可能被别人给覆盖了。

    三、懒汉式单例

    懒汉式单例:被外部类调用时才创建实例。

    例子:下面三种方式都可以。(线程安全)

    ① 粗粒度加锁:

    ② 细粒度加锁:(双重校验锁)

    提问:这两种懒汉式为什么成员变量不加final关键字?

    如果加final的话,singleton对象引用就不能被赋值了。

    静态内部类

    四、反射破坏单例

    提问:如下图代码,虽然以上懒汉式和饿汉式的构造方法都私有了,但还是没办法防止反射攻击,如何解决?

    反射攻击例子:

    解决:

    运行结果:

    五、序列化破坏单例

    当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,如下代码:

    编写测试代码:

    运行结果:

    从运行结果中可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加readResolve()方法即可,如下代码:

    再看运行结果:(原理这里就不解释了,实际上还是实例化了两次,只不过新创建的对象没有被返回)

    六、注册式单例

    注册式单例:将每一个实例都登记缓存到某一个地方中,使用唯一标识获取实例。

    注册式单例有两种:容器缓存枚举登记

    ① 先看枚举式单例的例子,创建一个EnumSingleton枚举类:(能防止序列化和反射攻击破坏单例)

    测试代码:

    运行结果:说明能防止序列化破坏单例。(枚举类型其实通过类名和Class对象找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次)

    再测试代码:

    运行结果:说明能防止反射攻击破坏单例。

    再测试代码:

    运行结果:这时错误更明显了,从JDK层面就明确规定不能用反射来创建枚举类型

    对EnumSingleton.class进行反编译,发现如下代码:

    枚举类单例在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例的体现。

    因此,《Effective Java》中推荐这种写法,从JDK层面就为枚举不被序列化和反射破坏来保驾护航。

    ② 再看容器缓存单例的写法,创建一个ContainerSingleton类:

    容器式写法适用于创建实例非常多的情况,便于管理。这里加上synchronized锁就是线程安全的。Spring中也用,如AbstractAutowireCapableBeanFactory。

    七、ThreadLocal线程单例

    ThreadLocal不能保证其创建的对象是全局唯一,但能保证在单个线程中是唯一的,且天生线程安全。如下代码:

    写一下测试代码:

    运行结果:(单个线程内才会是同个对象)

    上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。而ThreadLocal将所有对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

    八、知识重点总结

    1、私有化构造器

    2、保证线程安全

    3、延迟加载

    4、防止序列化和反序列化破坏单例

    5、防御反射攻击单例

  • 相关阅读:
    第五周学习总结-20175228
    第二周Java学习总结
    namke 命令行编译
    libssh2 的集成与应用
    vc6 编译问题
    vs2010 编译curl-7.42.1
    linux redis 安装
    解决error C2011: 'fd_set' : 'struct' type redefinition的方法
    ajax 的简单应用
    servlet 启动加载配置文件及初始化
  • 原文地址:https://www.cnblogs.com/ZekiChen/p/12448403.html
Copyright © 2020-2023  润新知