创建型模式(Creational Pattern)是对类的实例化过程的抽象化,能够提供对象的创建和管理职责。创建型模式共有5种:
■ 单例模式;
■ 工厂方法模式;
■ 抽象工厂模式;
■ 建造者模式;
■ 原型模式。
含义
单例模式的英文原话是:
Ensure a class has only one instance,and provide a global point of access to it.
单例模式的主要作用是确保一个类只有一个实例存在。单例模式可以用在建立目录、数据库连接、线程池等需要单线程操作的场合,用于实现对系统资源的控制。
单例类中一个最重要的特点是类的构造函数是私有的,从而避免外界利用构造函数直接创建出任意多的实例。另外需要注意的是,由于构造函数是私有的,因此该类不能被继承。
实现方法
懒汉式单例类
第一次引用类时,才进行对象实例化。
延迟加载,只有在真正使用的时候,才开始实例化
1 | public class LazySingleton { |
synchronized 保证多线程安全,如果直接加在方法上,不管有没有实例化都会加一次锁,会有性能的损耗,所以放在方法内部,用double check加锁优化,有点类似Peterson算法。
同时还要用volatile避免空指针问题。
为什么可能会发生空指针问题呢?先来看一下编译的原理,对于语句Demo demo = new Demo();
对编译后的.class文件用命令javap -v Demo.class
进行反汇编
可以看到标准的过程是:分配空间 -> 初始化 -> 引用赋值。
然而,由于JVM有即时编译功能,CPU也有流水线并行,可能第一个线程在未初始化之前先进行赋值,导致第二线程返回空指针, 所以用volatile 禁用字节码重排序。
饿汉式单例类
类加载时,就进行对象实例化
基于JVM类加载机制保证线程安全。 类加载的初始化阶段就完成了实例的初始化。
类加载的时机:当前类是启动类,直接进行new操作,访问静态属性/方法,用反射访问类,初始化一个类的子类等
类加载的过程:
- 加载二进制数据到内存中,生成对应的Class
- 连接:a.验证 b.准备(给类的静态成员变量赋默认值) c.解析
- 初始化:给类的静态变量赋初值
1 | public class HungrySingleton { |
静态内部类
基于JVM的懒加载机制,静态内部类被调用的时候才会被加载。如果是非静态内部类,在创建内部类实例之前,必须首先创建外围类的实例,所以用静态类比较合适
1 | public class InnerSingleton { |
反射攻击问题
以上三种方法存在的问题是,如果拿到了构造函数显示创建,仍然可创建出多个实例来,即反射攻击
以静态内部类为例:
1 | public class Test { |
对于饿汉模式和静态内部类,一种解决办法是在构造函数中添加判断条件。但是懒汉模式就不行了
1 | private InnerSingleton(){ |
通过查看newInstance()
源码,发现了一个判断条件,不能为枚举类型 , 说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
枚举类型
“单元素的枚举类型已经成为实现Singleton的最佳方法”
1 | public enum EnumSingleton { |
序列化问题
如果一个单例的类实现了Serializable或Cloneable接口,当反序列化或克隆时可能会创建出一个新的实例来,因为还原的时候并不会调用类的构造函数。
解决方法一是在序列化时加上一个版本号,同时实现readResolve()
方法,该方法返回单例对象
另一种解决方法仍然是使用枚举类型,天然支持反序列。
应用场景
■ 要求生成唯一序列号的环境。
■ 在整个项目中需要一个共享访问点或共享数据,例如,一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的。
■ 创建一个对象需要消耗的资源过多,如访问IO和数据库等资源。
■ 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(也可以采用直接声明为static的方式)。
如在Spring框架中,每个Bean 默认就是单例的;Java 基础类库中的 java.lang.Runtime 类也采用了单例模式,其getRuntime()方法返回了唯一的实例。
注意事项
■ 单例模式与单一职责原则有冲突。
■ 单例类仅局限于一个JVM,因此当多个JVM的分布式系统时,这个单例类就会在多个JVM中被实例化,造成多个单例对象的出现。如果是无状态的单例类,则没有问题,因为这些单例对象是没有区别的。如果是有状态的单例类,则会出现问题,例如,给系统提供一个唯一的序列号,此时序列号不唯一,可能出现多次。因此,在任何使用EJB、RMI和JINI技术的分布式系统中,应当避免使用有状态的单例类。
■ 同一个JVM中会有多个类加载器,当两个类加载器同时加载同一个类时,会出现两个实例,此时也应尽量避免使用有状态的单例类。
■ 使用单例模式时,需要注意序列化和克隆对实例唯一性的影响。如果一个单例的类实现了Serializable或Cloneable接口,则有可能被反序列化或克隆出一个新的实例来,从而破坏了“唯一实例”的要求,因此,通常单例类不需要实现Serializable和Cloneable接口。