一、单例模式
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、防御反射攻击单例