单例模式
- 单例模式是一种创建模式,单例类负责自己创建自己的对象并且一个类只有一个实例对象,并且向整个系统提供这个实例.系统可以直接访问这个实例而不需要实例化
-
单例模式的特点:
- 单例类只有一个实例
- 单例类必须自己创建自身的唯一实例
- 单例类必须给其余系统对象提供创建的唯一实例
单例模式的实现方式
- 单例模式要保证一个类只有一个实例,并且提供给全局访问,主要用于解决一个全局使用的类频繁创建和销毁的问题,通过判断系统是否存在这个单例来解决这样的问题,如果有这个单例则返回这个单例,否则就创建这个单例,只要保证构造函数是私有的即可
- 保证一个类只有一个实例: 将该类的构造方法定义为私有方法即可
- 提供全局一个该实例的访问点: 单例类自己创建实例,提供一个静态方法作为实例的访问点即可
-
饿汉和懒汉比较:
- 懒汉: 单例类对象实例懒加载,不会提前创建对象实例,只有在使用对象实例的时候才会创建对象实例
- 饿汉: 在单例对象实例进行声明引用时就进行实例化创建对象实例
- 单例模式除去线程不安全的懒汉,通常有五种实现方式:
- 懒汉
- 双检锁
- 饿汉
- 静态内部类
- 枚举
- 一般情况下,直接使用饿汉实现单例模式
- 如果明确要求懒加载通常使用静态内部类实现单例模式
- 如果有关于反序列化创建对象会考虑使用枚举实现单例模式
- 静态类Static :
- 静态类在第一次运行时直接初始化,也不需要在延迟加载中使用
- 在不需要维持任何状态,仅仅用于全局访问时,使用静态类的方式更加方便
- 如果需要被继承或者需要维持一些特定状态下的情况,就适合使用单例模式
线程不安全懒汉
- 单例模式线程不安全懒汉Singleton示例
- 使用了懒加载模式
- 存在当多个线程并行调用getInstance()方法时,会创建多个实例的问题.也就是说,在多线程模式下是无法正常工作的
线程安全懒汉
- 单例模式线程安全懒汉Singleton示例
- 解决了多线程环境下创建多个实例的问题
- 存在每次获取实例都需要申请锁的问题,方法效率低下,因为在任何时候只能有一个线程可以调用getInstance() 方法
双检锁
-
双重检查锁模式: doule checked locking pattern
- 使用同步块加锁的方法
- 会有两次检查instance == null
- 一次在同步块外
- 一次在同步块内
- 因为会有多个线程一起进入同步块外的if中
- 如果不在同步块内不进行二次检验就会导致生成多个实例
- 单例模式双检锁Singleton示例
-
volatile:
- 对于计算机中的指令而言 ,CPU和编译器为了提升程序的执行效率,通常会按照一定的规则对指令进行优化
- 如果两条指令互不依赖,那么指令执行的顺序可能不是源码的编写顺序
- 形如instance = new Instance() 方法创建实例执行分为三步:
- 分配对象内存空间: 给新创建的Instance对象分配内存
- 初始化对象: 调用单例类的构造函数来初始化成员变量
-
设置instance指向新创建的对象分配的内存地址,此时instance != null
- 因为上面的初始化对象和设置instance指向新创建的对象分配的内存地址不存在数据上的依赖关系,无论哪一步先执行都不会影响最终结果,所以程序在编译时,顺序就会发生改变:
- 分配对象内存空间
- 设置instance指向新创建对象分配的内存地址
- 初始化对象
-
CPU和编译器在指令重排时,不会关心指令重排执行是否影响多线程的执行结果. 如果不加volatile关键字,如果有多个线程访问getInstance() 方法时,如果刚好发生了指令重排,可能会出现以下情况:
- 当第一个线程获取锁并且进入到第二个if方法后,先分配内存空间,然后instance指向刚刚分配的内存地址,此时instance不等于null. 但是此时instance还没有初始化完成
- 如果此时有另一个线程调用getInstance() 方法,在第一个if的判断时结果就为false, 就会直接返回没有初始化完成的instance, 这样可能会导致程序NPE异常
- 因为上面的初始化对象和设置instance指向新创建的对象分配的内存地址不存在数据上的依赖关系,无论哪一步先执行都不会影响最终结果,所以程序在编译时,顺序就会发生改变:
-
使用volatile的原因是禁止指令重新排序:
- 在volatile变量进行赋值操作后会有一个内存隔离
- 读操作不会重排序到内存隔离之中
- 比如在上面操作中,读操作必须在执行完1,2,3或者1,3,2步骤之后才会执行读取到结果,否则不会读取到相关结果
饿汉
- 单例模式饿汉Singleton示例
-
优点:
- 在单例类中,装载类的时候就创建对象实例.因为单例类的实例声明为static的final变量,在第一次加在类到内存中时就会初始化,所以创建实例本身时线程安全的
-
缺点:
- 饿汉模式不是一种懒加载模式,即便客户端没有调用getInstance() 方法,单例类也会在类第一次加载时初始化
- 使用饿汉模式创建单例类实例在某些场景中无法使用:
- 比如因为饿汉创建的实例声明为final变量
- 如果单例类Singleton的实例的创建依赖参数或者配置文件
- 需要在getInstance() 方法之前调用方法为单例类的实例设置参数,此时这种饿汉模式就无法使用
静态内部类
- 单例模式静态内部类Singleton示例
- 使用静态内部类模式创建单例类实例是使用JVM机制保证线程安全:
- 静态单例对象没有作为单例类的成员变量直接实例化,所以当类加载时不会实例化单例类
- 第一次调用getInstance() 方法时将加载静态内部类Nest. 在静态内部类中定义了一个static类型的变量instance, 这时会首先初始化这个变量
- 通过JVM来保证线程安全,确保该成员变量只初始化一次
- 由于getInstance() 方法并没有加线程锁,所以对性能没有什么影响
-
静态内部类的优点:
- 静态内部类Nest是私有的,只能通过getInstance() 方法进行访问,所以这是懒加载的
- 读取实例时不会进行同步锁的获取,性能较好
- 静态内部类不依赖JDK版本
枚举
- 单例模式枚举Singleton示例
- 使用枚举方式实现单例的最大特点是非常简单
- 可以通过Enum.INSTANCE来访问实例,和getInstance() 方法比较更加简单
- 枚举的创建默认就是线程安全的方法,而且能防止反射以及反序列化导致重新创建新的对象
- Enum类内部使用Enum类型判定防止通过反射创建新的对象
- Enum类通过对象的类型和枚举名称将对象进行序列化,然后通过valueOf() 方法匹配枚举名称找到内存中的唯一对象实例,这样可以防止反序列化时创建新的对象
- 懒汉式和饿汉式实现的单例模式破坏 : 无论是通过懒汉式还是饿汉式实现的单例模式,都可能通过反射和反序列化破坏掉单例的特性,可以创建多个对象
- 反射破坏单例模式: 利用反射,可以强制访问单例类的私有构造器,创建新的对象
public static void main(String[] args) { // 利用反射获取单例类的构造器 Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); // 设置访问私有构造器 constructor.setAccessiable(true); // 利用反射创建新的对象 Singleton newInstance = constructor.newInstance(); // 通过单例模式创建单例对象 Singleton singletonInstance = Singleton.getInstance(); // 此时这两个对象是两个不同的对象,返回false System.out.println(singletonInstance == newInstance); }
- 反序列化破坏单例模式: 通过readObject() 方法读取对象时会返回一个新的对象实例
public static void main(String[] args) { // 创建一个输出流对象 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Singleton.file")); // 将单例类对象写入到文件中 Singleton singletonInstance = Singleton.getInstance(); os.writeObject(singleton); // 从文件中读取单例对象 File file = new File("Singleton.file"); ObjectInputStream is = new ObjectInputStream(new FileInputStream(file)); Singleton newInstance = (Singleton)is.readObject(); // 此时这两个对象是两个不同的对象,返回false System.out.println(singletonInstance == newInstance); }