Java设计模式优化单例模式

2次阅读

共计 3510 个字符,预计需要花费 9 分钟才能阅读完成。

单例模式概述

单例模式是一种对象创建模式,用于产生一个类的具体事例。使用单例模式可以确保整个系统中单例类只产生一个实例。有下面两大好处:

  1. 对于频繁创建的对象,节省初第一次实例化之后的创建时间。
  2. 由于 new 操作的减少,会降低系统内存的使用频率。减轻 GC 压力,从而缩短 GC 停顿时间

创建方式:

  1. 单例作为类的私有 private 属性
  2. 单例类拥有私有 private 构造函数
  3. 提供获取实例的 public 方法

单例模式的角色:

角色 作用
单例类 提供单例的工厂,返回类的单例实例
使用者 获取并使用单例类

类基本结构:

单例模式的实现

1. 饿汉式

public class HungerSingleton {
    //1. 饿汉式
    // 私有构造器
    private HungerSingleton() {System.out.println("create HungerSingleton");
    }
    // 私有单例属性
    private static HungerSingleton instance = new HungerSingleton();
    // 获取单例的方法
    public static HungerSingleton getInstance() {return instance;}
}

注意:

  1. 单例修饰符为 static JVM 加载单例类加载时,直接初始化单例。无法延时加载。如果此单例一直未被使用,单 Singleton 因为调用静态方法被初始化则会造成内存的浪费。
  2. getInstance()使用 static 修饰,不用实例化可以直接使用 Singleton.getInstance()获取单例。
  3. 由于单例由 JVM 加载类的时候创建,所以不存在 线程安全 问题。

2. 简单懒汉式

public class Singleton {//2.1 简单懒汉式(线程不安全)
    // 私有构造器
    private Singleton() {System.out.println("create Singleton");
    }
    // 私有单例属性[初始化为 null]
    private static Singleton instance = null;
    // 获取单例的方法
    public static Singleton getInstance() {if(instance == null) {
            // 此处 instance 实例化
            // 首次调用单例时会进入  达成延时加载
            instance = new Singleton();}
        return instance;
    }
}
  • 由于未使用 synchronized 关键字,所以当线程 1 调用单例工厂方法 Singleton.getInstance() 且 instance 未初始化完成时,线程 2 调用此方法会将 instance 判断为 null,也会将 instance 重新实例化赋值,此时则产生了多个实例!
  • 如需线程安全可以直接给 getInstance 方法上加 synchronized 关键字, 如下:
public class Singleton {//2.2 简单懒汉式(线程安全)
    // 私有构造器
    private Singleton() {System.out.println("create Singleton");
    }
    // 私有单例属性[初始化为 null]
    private static Singleton instance = null;
    // 获取单例的方法 将此方法使用 synchronized 关键字同步
    public static synchronized Singleton getInstance() {if(instance == null) {
            // 此处 instance 实例化
            // 首次调用单例时会进入  达成延时加载
            instance = new Singleton();}
        return instance;
    }
}

面临的问题:

  • 由于对 getInstance()整个方法加锁,在多线程的环境中性能比较差。

3.DCL 懒汉式(双重检测)

简单懒汉式(线程安全)中,对 getInstance()方法加锁,导致多线程中性能较差,那么是否可以 减小锁的范围 ,使不用每次调用 geInstance() 方法时候都会去竞争锁?

DCL(Double Check Locking)双重检测 就是这样一种实现方式。

传统 DCL:

public class DCLLazySingleton {
    //3.DCL
    // 私有构造器
    private DCLLazySingleton() {System.out.println("create DCLLazySingleton");
    }
    //step1 私有单例属性[初始化为 null] volatile 保证内存可见性 防止指令重排
    private static volatile DCLLazySingleton instance = null;
    // 获取单例的方法
    public static DCLLazySingleton getInstance() {
        // 这里判 null 是为了在 instance 有值时,不进入加锁的代码块,提高代码性能。if(instance == null) {
            // 缩小锁范围 由于是静态方法方法调用的时候不依赖于实例化的对象 加锁只能使用类
            synchronized (DCLLazySingleton.class) {
                // 这里判 null 是为了配合 volatile 解决多线程安全问题
                if(instance == null) {instance = new DCLLazySingleton();
                }
            }
        }
        return instance;
    }
}

注意:

  1. 传统 DCL(未使用 volatile 或在 JDK1.8 之前版本)面临的问题:

    • 由于初始化单例对象 new DCLLazySingleton() 操作 并不是原子操作,由于这是很多条指令,jvm 可能会乱序执行。

在线程 1 初始化对象可能并未完成,但是此时已经 instance 对象已经不为 null。(已经分配了内存,但是构造方法还未执行完【可能有一些属性的赋值未执行 】)
此时线程 2 再获取 instance 则不为 null 直接返回。那么此时线程 2 获取的则为‘构造方法未执行完的 instance 对象’。则不能保证线程安全。

  1. 解决方式:

    • 加上 volatile 关键字,volatile 保证内存可见性,内存屏障,防止指令排!
    • 加上 volatile 关键字后,线程 2 获取的构造方法未执行完的 instance 对象,会在线程 1 修改之后同步到线程 2(volatile 内存空间)。所以解决了线程安全问题
  2. 参考:

    • DCL 失效原因和解决方案
    • java 中单例模式 DCL 的缺陷及单例的正确写法

4. 懒汉式(静态内部类)

public class StaticSingleton {
    // 私有构造器
    private StaticSingleton() {System.out.println("create StaticSingleton!");
    }
    // 获取单例的方法
    public static StaticSingleton getInstance() {return SingletonHolder.instance;}
    // 静态内部类 持有单例 作为静态属性。// 由于只有在访问属性时才会加载静态类初始化 instance。所以实现了懒加载。且由于 JVM 保证了类的加载为线程安全,所以为线程安全的。private static class SingletonHolder {
        // 私有单例属性
        private static StaticSingleton instance = new StaticSingleton();}
}

注意:

  1. 由于 StaticSingleton 类被加载时,内部的私有静态类 SingletonHolder 并不会被加载,所以并不会初始化单例 instance,当 getInstance()被调用时 SingletonHolder.instance 才会加载 SingletonHolder,由于 JVM 保证了类的加载为线程安全,因此线程安全。
  2. 此方式既可以做到延时加载,也不会因为同步关键字影响性能。是一种比较完善的实现。推荐使用

5. 枚举单例

public enum EnumSingleton {INSTANCE();
    EnumSingleton() {System.out.println("create EnumSingleton");
    }
}
  • 线程安全,且能够抵御反射与序列化。
  • 推荐使用

例外情况

上述的单例实现方式还是会面临一些特殊情况不能保证唯一实例:

  1. 反射调用私有构造方法。
  2. 序列化后反序列化会生成多个对象。可以实现私有 readResolve 方法。readObject()如同虚设,直接使用 readResolve 替换原本返回值。如下:
    private Object readResolve () {
    // 返回当前对象
    return instance;
    }

由于上述两情况比较特殊,所以没有特别关注。

参考书籍


《Java 程序性能优化》- 葛一鸣 等编著

正文完
 0