说明

定义:保证一个类只有一个实例,并且提供一个全局访问点

场景:

  • 重量级的对象,不需要多个实例,如线程池,数据库连接池等
  • Java中的应用:Runtime

懒加载模式

懒加载模式:延迟加载,实例在需要时才实例化

  1. // 单线程模式,非多线程安全
  2. public class LazySingleton {
  3. private static LazySingleton instance;
  4. // 避免LazySingleton通过空构造实例化
  5. private LazySingleton() {
  6. }
  7. public static LazySingleton getInstance() {
  8. if (instance == null) {
  9. // 多线程测试,放大问题发生可能性!
  10. try {
  11. TimeUnit.SECONDS.sleep(1);
  12. } catch (InterruptedException e) {
  13. e.printStackTrace();
  14. }
  15. instance = new LazySingleton();
  16. }
  17. return instance;
  18. }
  19. }
  20. // 测试
  21. public class LazySingletonTest {
  22. public static void main(String[] args) {
  23. // 单线程测试
  24. // LazySingleton lazySingleton=LazySingleton.getInstance();
  25. // LazySingleton lazySingleton1=LazySingleton.getInstance();
  26. //
  27. // System.out.println(lazySingleton == lazySingleton1);
  28. // 多线程环境
  29. new Thread(() -> {
  30. LazySingleton instance = LazySingleton.getInstance();
  31. System.out.println(instance);
  32. }).start();
  33. new Thread(() -> {
  34. LazySingleton instance = LazySingleton.getInstance();
  35. System.out.println(instance);
  36. }).start();
  37. }
  38. }

避免多线程导致的问题:

方案一:使用 synchronized 对 getInstance() 方法加锁,优点是实现简单,能避免多线程导致的安全问题,缺点是每次访问都需要加锁,不管实例有没有被初始化,影响效率

  1. public class LazySingletonMultiThread {
  2. // volatile修饰,防止指令重排序
  3. private volatile static LazySingletonMultiThread instance;
  4. // 避免LazySingleton通过空构造实例化
  5. private LazySingletonMultiThread() {
  6. }
  7. /**
  8. * 方案一:
  9. * 对getInstance加锁,就能够实现多线程环境下的单例模式
  10. * 缺点:每次访问都需要加锁,不管实例有没有被初始化,浪费资源和时间
  11. * @return
  12. */
  13. public static synchronized LazySingletonMultiThread getInstance(){
  14. if (instance == null){
  15. instance=new LazySingletonMultiThread();
  16. }
  17. return instance;
  18. }
  19. }

方案二:仅对实例化步骤进行加锁,由于多个线程可能同时同步在实例化锁,因此在实例化前需要进行判断。

缺点:在字节码层面,即实例化阶段,由于可能发生的指令重排,赋值发生在初始化之前,导致下一个线程获取到空指针的现象。因此,需要volatile来修饰实例,避免指令重排。

  1. public class LazySingletonMultiThread {
  2. // volatile修饰,防止指令重排序
  3. private volatile static LazySingletonMultiThread instance;
  4. // 避免LazySingleton通过空构造实例化
  5. private LazySingletonMultiThread() {
  6. }
  7. /**
  8. * 方案二:
  9. * 先判断instance是否为空,如果不为空,则不需要加锁,仅对实例化过程进行加锁;
  10. *
  11. * @return
  12. */
  13. public static LazySingletonMultiThread getInstance() {
  14. if (instance == null) {
  15. /**
  16. * 可能同时存在多个线程进入 synchronized 代码块,使得实例化多次,
  17. * 因此需要在synchronized内部对instance进行判断,避免多次实例化
  18. */
  19. synchronized (LazySingletonMultiThread.class) {
  20. if (instance == null) {
  21. instance = new LazySingletonMultiThread();
  22. /**
  23. * 字节码层面:
  24. * 可能发生指令重排:
  25. * JIT:Just In Time 即时编译
  26. * CPU
  27. * 1. 分配空间
  28. * 2. 初始化
  29. * 3. 引用赋值
  30. *
  31. * 存在的问题:可能由于指令重排,导致引用赋值和初始化顺序颠倒,
  32. * 而下一个过来的线程由于引用已经赋值,直接返回,存在获取到空指针的现象.
  33. *
  34. * 因此,需要volatile来修饰实例,避免指令重排。
  35. */
  36. }
  37. }
  38. }
  39. return instance;
  40. }
  41. }

饿汉模式

类加载在类的初始化阶段就已经完成了类的实例化,本质上就是借助于jvm的类加载机制,保证实例的唯一性。

类加载过程:

  • 加载二进制数据到内存中,生成对应的Class数据结构;
  • 连接:
    • 验证
    • 准备:给类的静态成员变量赋默认值
    • 解析
  • 初始化:给类的静态变量赋初始值;

只有在真正使用对应的类时,才会触发初始化。如:当前类是启动类即main函数所在类,直接进行new操作,访问静态属性、访问静态方法,用反射访问类,初始化一个类的子类等。

  1. public class HungrySingleton {
  2. // 在类加载最后一步初始化阶段被实例化
  3. private static HungrySingleton instance = new HungrySingleton();
  4. private HungrySingleton() {
  5. }
  6. public static HungrySingleton getInstance() {
  7. return instance;
  8. }
  9. public static void main(String[] args) {
  10. HungrySingleton instance = HungrySingleton.getInstance();
  11. HungrySingleton instance2 = HungrySingleton.getInstance();
  12. System.out.println(instance == instance2);
  13. }
  14. }

静态内部类

  1. 本质上是利用类的加载机制来保证线程安全;
  2. 只有在实际使用的时候,即调用getInstance()方法时才会触发类的初始化,属于懒加载的一种形式。

    1. public class InnerClassSingleton {
    2. private static class InnerClassHolder {
    3. private static InnerClassSingleton instance = new InnerClassSingleton();
    4. }
    5. private InnerClassSingleton() {
    6. }
    7. /**
    8. * 实现了一个懒加载单例模式,该单例在调用getInstance()方法时被实例化,
    9. * 即在调用getInstance时,JVM会加载该内部类完成实例化,利用JVM的类加载机制实现线程安全
    10. *
    11. * @return 单例
    12. */
    13. public static InnerClassSingleton getInstance() {
    14. return InnerClassHolder.instance;
    15. }
    16. }

反射攻击实例

  1. public class Test {
  2. public static void main(String[] args) throws NoSuchMethodException,IllegalAccessException, InvocationTargetException, InstantiationException {
  3. // 获取构造方法
  4. Constructor<InnerClassSingleton> constructor = InnerClassSingleton.class.getDeclaredConstructor();
  5. // 获取权限
  6. constructor.setAccessible(true);
  7. // 通过反射使用构造函数实例化
  8. InnerClassSingleton innerClassSingleton = constructor.newInstance();
  9. // 使用getInstance()方法实例化
  10. InnerClassSingleton innerClassSingleton1=InnerClassSingleton.getInstance();
  11. System.out.println(innerClassSingleton == innerClassSingleton1); // false
  12. }
  13. }

为了避免使用反射创建多实例,饿汉模式和内部类模式可以在构造函数中进行判断,抛出异常。

  1. public class SingletonMode {
  2. private final static SingletonMode instance = new SingletonMode();
  3. private SingletonMode() {
  4. if (instance != null){
  5. throw new RuntimeException("单例模式不允许创建多个实例");
  6. }
  7. }
  8. public static SingletonMode getInstance(){
  9. return instance;
  10. }
  11. }

枚举实现单例模式

通过JVM保证线程安全和反射安全;

  1. public enum EnumSingleton {
  2. INSTANCE;
  3. public void print() {
  4. System.out.println("this.hashCode() = " + this.hashCode());
  5. }
  6. }

序列化和反序列化

  • 对不是枚举实现的单例模式单例的序列化和反序列化会破坏单例模式的实例唯一性,因此需要对单例进行处理:序列化和反序列化需要实现Serializable接口。
  • 枚举类是天然的单例模式 ```java // 对内部类实现的单例模式进行改造 public class InnerClassSingletonSerializable implements Serializable { // 版本号 private static final long serialVersionUID = 42L;

    // 相当于方法签名 private Object readResolve() throws ObjectStreamException {

    1. return InnerClassSingletonSerializable.InnerClassHolder.instance;

    }

    private static class InnerClassHolder {

    1. private static InnerClassSingletonSerializable instance = new InnerClassSingletonSerializable();

    }

    private InnerClassSingletonSerializable() {

    1. /**
    2. * 防止通过反射创建实例!
    3. */
    4. if (InnerClassSingletonSerializable.InnerClassHolder.instance != null) {
    5. throw new RuntimeException("单例不允许多个实例!");
    6. }

    }

    /**

    • 实现了一个懒加载单例模式,该单例在调用getInstance()方法时被实例化,
    • 即在调用getInstance时,JVM会加载该内部类完成实例化,利用JVM的类加载机制实现线程安全 *
    • @return 单例 */ public static InnerClassSingletonSerializable getInstance() { return InnerClassSingletonSerializable.InnerClassHolder.instance; } }

//测试: public static void main(String[] args) throws IOException, ClassNotFoundException {

  1. // 序列化会破坏单例模式
  2. // 写入单例模式实例
  3. InnerClassSingleton instance = InnerClassSingleton.getInstance();
  4. ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("test"));
  5. oos.writeObject(instance);
  6. oos.close();
  7. // 读取单例实例
  8. ObjectInputStream ois=new ObjectInputStream(new FileInputStream("test"));
  9. InnerClassSingleton singleton = (InnerClassSingleton) ois.readObject();
  10. ois.close();
  11. // 对比序列化前后是否为同一实例
  12. System.out.println("(instance==singleton) = " + (instance == singleton)); // false:破坏了单例
  13. // 写入单例模式实例,经过修改后的单例
  14. InnerClassSingletonSerializable instanceSerializable = InnerClassSingletonSerializable.getInstance();
  15. ObjectOutputStream oos1=new ObjectOutputStream(new FileOutputStream("testSerializable"));
  16. oos1.writeObject(instanceSerializable);
  17. oos1.close();
  18. // 读取单例实例
  19. ObjectInputStream ois1=new ObjectInputStream(new FileInputStream("testSerializable"));
  20. InnerClassSingletonSerializable singletonSerializable = (InnerClassSingletonSerializable) ois1.readObject();
  21. ois1.close();
  22. // 对比序列化前后是否为同一实例
  23. System.out.println("(instanceSerializable == singletonSerializable) = " + (instanceSerializable == singletonSerializable));

} // 输出: (instance==singleton) = false (instanceSerializable == singletonSerializable) = true ```