是什么?

单例的意思就是,内存中 只有一个对象存在,是一种创建对象的方式。
只有一个对象存在可以减少资源消耗,生活中大多数场景都是单例的,可以公共使用的对象一般都可以用单例来实现,比如公司的打印机、咖啡机,一般都是一台,如果给每个人都配置一台,有些资源浪费。

适用场景?

可以共用的对象,无需每次使用都new对象,节约资源。
注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。
  • 4、 注意多线程下并发时的线程安全问题

    代码实现

    懒汉模式实现

    单例模式最容易想到的就是第一次获取对象时创建,也就是延时加载,又称为懒汉模式。
    最简单的写法如下: ```java

/**

  • 单例模式-懒汉模式
  • 懒加载实现,首次调用时创建对象 */ public class Single { //静态成员变量 private static Single instance;

    //私有化构造方法 这样保证该类无法通过new实例化 private Single() { }

    /**

    • 获取实例
    • 线程不安全 */ public static Single getInstance() { if (instance == null) {
      1. instance = new Single();
      } return instance; } }

//测试代码 //单线程安全 Single instance1 = Single.getInstance(); Single instance2 = Single.getInstance(); System.out.println(instance1 == instance2); //输出:true

//多线程不安全 new Thread(() -> System.out.println(Single.getInstance())).start(); new Thread(() -> System.out.println(Single.getInstance())).start(); //输出: //com.initit.单例模式SinglePattern.single1懒汉模式单线程.Single@72679c71 //com.initit.单例模式SinglePattern.single1懒汉模式单线程.Single@3ebb0609

  1. 以上方式单线程下简单高效,多线程模式下会出现问题,优化改进如下
  2. ```java
  3. /**
  4. * 获取实例
  5. * synchronized保证线程安全
  6. */
  7. public static synchronized Single getInstance() {
  8. if (instance == null) {
  9. instance = new Single();
  10. }
  11. return instance;
  12. }

使用synchronized关键字后,可以保证线程安全,但是每次线程访问都加锁,会影响效率,可以再进一步优化,只再第一次创建对象的时候加线程锁即可,如下:

  1. /**
  2. * 获取实例
  3. * 高性且线程安全
  4. */
  5. public static Single getInstance() {
  6. if (instance == null) {
  7. synchronized (Single.class) {
  8. if (instance == null) {
  9. instance = new Single();
  10. }
  11. }
  12. }
  13. return instance;
  14. }

加锁优化后,double check,判定两次,第一次对象为空时加锁,如果此时并发创建对象,由第二次判定保证只实例化一次,改方案比较完美了,但是仔细分析new对象的过程,CPU可能会堆new对象的指令重新排序,另外JVM对热点代码会进行JIT(即时编译)也可能导致指令重排,故还能进一步优化,增加volatile关键字。
new对象的过程

  1. 分配内存空间
  2. 初始化
  3. 引用赋值 ```java /**

    • 单例模式-懒汉模式
    • 懒加载实现,首次调用时创建对象 */ public class Single { //静态成员变量 volatile内存可见性 private static volatile Single instance;

      //私有化构造方法 这样保证该类无法通过new实例化 private Single() { }

      /**

      • 获取实例
      • 高性且线程安全 */ public static Single getInstance() { if (instance == null) {
        1. synchronized (Single.class) {
        2. if (instance == null) {
        3. //new对象的过程
        4. //1. 分配内存空间
        5. //2. 初始化
        6. //3. 引用赋值
        7. instance = new Single();
        8. }
        9. }
        } return instance; }

}

  1. <a name="qpXGf"></a>
  2. ## 饿汉模式实现
  3. 饿汉模式实现,使用JVM的类加载机制实现单理,由类加载时创建对象实例<br />类加载顺序:
  4. 1. 加载字节码到内存中,生产对应的Class数据结构
  5. 1. 连接:验证->准备(给类的静态成员变量赋默认值(零值))->解析
  6. 1. 初始化:给类的静态成员变量赋初值
  7. 由以上3步骤赋初值时创建对象实例,由JVM保证线程安全。<br />有点是:线程安全,高效<br />缺点是:浪费内存,不管对象是否使用都创建
  8. ```java
  9. /**
  10. * 单例模式-简单实现
  11. * <p>
  12. * 常量方式实现,类加载时就创建对象实例,线程安全,效率高,
  13. * 但是浪费内存,不管对象是否使用都创建对象
  14. */
  15. public class Single {
  16. //静态常量 类加载时创建对象实例,只会创建一次
  17. private static final Single INSTANCE = new Single();
  18. //私有化构造方法 这样保证该类无法通过new实例化
  19. private Single() {
  20. }
  21. //获取实例方法,每次返回的都是同一个方法的引用,实现单例模式
  22. public static Single getInstance() {
  23. return INSTANCE;
  24. }
  25. }

内部类模式实现

饿汉模式不管对象是否使用都创建,能否改进为首次调用时创建对象呢?饿汉模式是类加载时创建对象,由JVM的类加载机制保证单例,想要首次调用时才加载类,使用内部类方式即可,,只有外部类通过显示的调用个体Instance()时,内部类才会被加载,规避了饿汉模式用不用都创建对象的缺点,只在第一次调用对象时才创建,也是一种懒加载模式,高效且节省内存空间。

  1. /**
  2. * 单例模式-内部类方式实现
  3. * <p>
  4. * 使用内部类实现
  5. * 利用了java的类加载机制,类装载时单线程创建对象
  6. * 而且类加载时,Single4被加载了,SingleHolder类没有被加载,
  7. * 只有通过显式调用getInstance()方法时,对象才会实例化,
  8. * 完美实现了懒加载,适合对象实例化非常耗费资源的情况
  9. */
  10. public class Single {
  11. //私有化内部类
  12. private static class SingleHolder {
  13. private static final Single INSTANCE = new Single();
  14. }
  15. //私有化静态方法
  16. private Single() {
  17. }
  18. /**
  19. * 获取对象实例方法
  20. */
  21. public static Single getInstance() {
  22. return SingleHolder.INSTANCE;
  23. }
  24. }

枚举模式实现

JVM提供了枚举模式,最简单的实现单例模式,同时JVM保证了反射时也能保证单例安全。

  1. /**
  2. * 单例模式-枚举方式实现
  3. * 枚举本质上是一个interface,所有的属性都是常量
  4. * 而且每一个常量都是一个类的对象
  5. * 所有枚举本身就实现了单例模式
  6. */
  7. public enum Single {
  8. INSTANCE;
  9. public Single getInstance() {
  10. return INSTANCE;
  11. }
  12. }

特别说明

聪明的你是否有了一些疑问?
既然饿汉模式是依赖JVM的类加载机制实现的,那么我绕过类加载机制去创建对象呢?
比如对象序列化,比如反射?
懒汉模式是否也可以反射去破坏单例呢?
JVM的枚举是怎么保证单例的,能否反射破坏?能否序列化破坏?
其他情况呢?

反射攻击

如果我不适用new的方式(即类加载方式)获取对象,直接使用反射创建对象,还能保证单例吗?

  1. //反射攻击
  2. Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor();
  3. Single single = declaredConstructor.newInstance();
  4. System.out.println(single==Single.getInstance());
  5. //输出: false

对于饿汉模式,即静态成员变量方式实现单例模式的(饿汉模式+内部类模式),可以自定义私有的构造器,内部增加成员变量判空操作,如果成员变量不为空之间抛出异常即可

  1. //私有化构造方法 这样保证该类无法通过new实例化
  2. private Single() {
  3. if (null != INSTANCE) {
  4. throw new RuntimeException("单例不能被多次实例化");
  5. }
  6. }
  7. //反射方式创单对象时抛出异常
  8. //Exception in thread "main" java.lang.reflect.InvocationTargetException
  9. // at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  10. // at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
  11. // at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
  12. // at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  13. // at com.initit.单例模式SinglePattern.single5_饿汉模式.Single.main(Single.java:31)
  14. //Caused by: java.lang.RuntimeException: 单例不能被多次实例化
  15. // at com.initit.单例模式SinglePattern.single5_饿汉模式.Single.<init>(Single.java:19)
  16. // ... 5 more

对于懒汉模式,是无法防止反射方式破坏单例模式的

来看一下反射创建对象的本质,核心代码为newInstance()方法,如下截图:
image.png
可以看到,如果是枚举是不允许反射创建对象的,JVM对反射提供了单例安全。

序列化攻击

除了反射,序列化也是一种创建对象的方式,我们来看下序列化创建对象是否可以保证单例?

  1. //提供序列化
  2. public class Single implements Serializable {
  3. //类修改后 只要serialVersionUID一致,就可以反序列化,否则报错
  4. private static final long serialVersionUID = 1L;
  5. //……省略其他代码
  1. //序列化攻击
  2. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleInstance"));
  3. oos.writeObject(INSTANCE);
  4. oos.close();
  5. ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleInstance"));
  6. Single o = (Single) ois.readObject();
  7. ois.close();
  8. System.out.println(o==INSTANCE);
  9. //输出: false

如上代码,序列化时不能保证单例,那怎么办呢?JVM规范给了解决方案,如下:
image.png

  1. //增加此方法,序列化时也可以保证单例
  2. Object readResolve() throws ObjectStreamException {
  3. return INSTANCE;//返回你定义的单例对象
  4. }

核心原理是什么?看下 ois.readObject();方法,JVM读取字节码创建对象时怎么做的?
image.png
image.png
核心方法实现是一个switch语句,是枚举和对象时,如上截图,看下源码,
image.png
判断自定义了readResolve() 方法,直接返回用户自定义对象。
image.png
枚举是使用valueof()方法获取的对象,JVM保证其线程安全和单例。

为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文如下:

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent. 大概意思就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:

  1. public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
  2. T result = enumType.enumConstantDirectory().get(name);
  3. if (result != null)
  4. return result;
  5. if (name == null)
  6. throw new NullPointerException("Name is null");
  7. throw new IllegalArgumentException(
  8. "No enum const " + enumType +"." + name);
  9. }

从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。
所以,JVM对序列化有保证。

扩展:枚举的字节码文件

需要看下枚举类的字节码文件,我们来回顾一下javap命令

  1. javap
  2. 用法: javap <options> <classes>
  3. 其中, 可能的选项包括:
  4. -help --help -? 输出此用法消息
  5. -version 版本信息
  6. -v -verbose 输出附加信息
  7. -l 输出行号和本地变量表
  8. -public 仅显示公共类和成员
  9. -protected 显示受保护的/公共类和成员
  10. -package 显示程序包/受保护的/公共类
  11. 和成员 (默认)
  12. -p -private 显示所有类和成员
  13. -c 对代码进行反汇编
  14. -s 输出内部类型签名
  15. -sysinfo 显示正在处理的类的
  16. 系统信息 (路径, 大小, 日期, MD5 散列)
  17. -constants 显示最终常量
  18. -classpath <path> 指定查找用户类文件的位置
  19. -cp <path> 指定查找用户类文件的位置
  20. -bootclasspath <path> 覆盖引导类文件的位置

我们编写一个枚举类单例示例:

  1. /**
  2. * 单例模式-枚举方式实现
  3. * 枚举本质上是一个interface,所有的属性都是常量
  4. * 而且每一个常量都是一个类的对象
  5. * 所有枚举本身就实现了单例模式
  6. */
  7. public enum Single {
  8. INSTANCE;
  9. public Single getInstance() {
  10. return INSTANCE;
  11. }
  12. }

javap反编译字节码文件得:

  1. javap Single.class
  2. Compiled from "Single.java"
  3. public final class com.initit.单例模式SinglePattern.single7_枚举实现.Single extends java.lang.Enum<com.initit.单例模式SinglePattern.single7_枚举实现.Single> {
  4. public static final com.initit.单例模式SinglePattern.single7_枚举实现.Single INSTANCE;
  5. public static com.initit.单例模式SinglePattern.single7_枚举实现.Single[] values();
  6. public static com.initit.单例模式SinglePattern.single7_枚举实现.Single valueOf(java.lang.String);
  7. static {};
  8. }

javap -v 显示辅助信息,如下

  1. javap -v Single.class
  2. Classfile /C:/WorkSpace/my-study/DesignPattern/target/classes/com/initit/单例模式SinglePattern/single7_枚举实现/Single.class
  3. Last modified 2021-7-12; size 1149 bytes
  4. MD5 checksum 2766bd70c1bef63fe43dda3f2831a462
  5. Compiled from "Single.java"
  6. public final class com.initit.单例模式SinglePattern.single7_枚举实现.Single extends java.lang.Enum<com.initit.单例模式SinglePattern.single7_枚举实现.Single>
  7. minor version: 0
  8. major version: 52
  9. flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
  10. Constant pool:
  11. #1 = Fieldref #4.#33 // com/initit/单例模式SinglePattern/single7_枚举实现/Single.$VALUES:[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  12. #2 = Methodref #34.#35 // "[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;".clone:()Ljava/lang/Object;
  13. #3 = Class #14 // "[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;"
  14. #4 = Class #36 // com/initit/单例模式SinglePattern/single7_枚举实现/Single
  15. #5 = Methodref #10.#37 // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  16. #6 = Methodref #10.#38 // java/lang/Enum."<init>":(Ljava/lang/String;I)V
  17. #7 = String #11 // INSTANCE
  18. #8 = Methodref #4.#38 // com/initit/单例模式SinglePattern/single7_枚举实现/Single."<init>":(Ljava/lang/String;I)V
  19. #9 = Fieldref #4.#39 // com/initit/单例模式SinglePattern/single7_枚举实现/Single.INSTANCE:Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  20. #10 = Class #40 // java/lang/Enum
  21. #11 = Utf8 INSTANCE
  22. #12 = Utf8 Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  23. #13 = Utf8 $VALUES
  24. #14 = Utf8 [Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  25. #15 = Utf8 values
  26. #16 = Utf8 ()[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  27. #17 = Utf8 Code
  28. #18 = Utf8 LineNumberTable
  29. #19 = Utf8 valueOf
  30. #20 = Utf8 (Ljava/lang/String;)Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  31. #21 = Utf8 LocalVariableTable
  32. #22 = Utf8 name
  33. #23 = Utf8 Ljava/lang/String;
  34. #24 = Utf8 <init>
  35. #25 = Utf8 (Ljava/lang/String;I)V
  36. #26 = Utf8 this
  37. #27 = Utf8 Signature
  38. #28 = Utf8 ()V
  39. #29 = Utf8 <clinit>
  40. #30 = Utf8 Ljava/lang/Enum<Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;>;
  41. #31 = Utf8 SourceFile
  42. #32 = Utf8 Single.java
  43. #33 = NameAndType #13:#14 // $VALUES:[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  44. #34 = Class #14 // "[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;"
  45. #35 = NameAndType #41:#42 // clone:()Ljava/lang/Object;
  46. #36 = Utf8 com/initit/单例模式SinglePattern/single7_枚举实现/Single
  47. #37 = NameAndType #19:#43 // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  48. #38 = NameAndType #24:#25 // "<init>":(Ljava/lang/String;I)V
  49. #39 = NameAndType #11:#12 // INSTANCE:Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  50. #40 = Utf8 java/lang/Enum
  51. #41 = Utf8 clone
  52. #42 = Utf8 ()Ljava/lang/Object;
  53. #43 = Utf8 (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  54. {
  55. public static final com.initit.单例模式SinglePattern.single7_枚举实现.Single INSTANCE;
  56. descriptor: Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  57. flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
  58. public static com.initit.单例模式SinglePattern.single7_枚举实现.Single[] values();
  59. descriptor: ()[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  60. flags: ACC_PUBLIC, ACC_STATIC
  61. Code:
  62. stack=1, locals=0, args_size=0
  63. 0: getstatic #1 // Field $VALUES:[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  64. 3: invokevirtual #2 // Method "[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;".clone:()Ljava/lang/Object;
  65. 6: checkcast #3 // class "[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;"
  66. 9: areturn
  67. LineNumberTable:
  68. line 9: 0
  69. public static com.initit.单例模式SinglePattern.single7_枚举实现.Single valueOf(java.lang.String);
  70. descriptor: (Ljava/lang/String;)Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  71. flags: ACC_PUBLIC, ACC_STATIC
  72. Code:
  73. stack=2, locals=1, args_size=1
  74. 0: ldc #4 // class com/initit/单例模式SinglePattern/single7_枚举实现/Single
  75. 2: aload_0
  76. 3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
  77. 6: checkcast #4 // class com/initit/单例模式SinglePattern/single7_枚举实现/Single
  78. 9: areturn
  79. LineNumberTable:
  80. line 9: 0
  81. LocalVariableTable:
  82. Start Length Slot Name Signature
  83. 0 10 0 name Ljava/lang/String;
  84. static {};
  85. descriptor: ()V
  86. flags: ACC_STATIC
  87. Code:
  88. stack=4, locals=0, args_size=0
  89. 0: new #4 // class com/initit/单例模式SinglePattern/single7_枚举实现/Single
  90. 3: dup
  91. 4: ldc #7 // String INSTANCE
  92. 6: iconst_0
  93. 7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
  94. 10: putstatic #9 // Field INSTANCE:Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  95. 13: iconst_1
  96. 14: anewarray #4 // class com/initit/单例模式SinglePattern/single7_枚举实现/Single
  97. 17: dup
  98. 18: iconst_0
  99. 19: getstatic #9 // Field INSTANCE:Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  100. 22: aastore
  101. 23: putstatic #1 // Field $VALUES:[Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;
  102. 26: return
  103. LineNumberTable:
  104. line 10: 0
  105. line 9: 13
  106. }
  107. Signature: #30 // Ljava/lang/Enum<Lcom/initit/单例模式SinglePattern/single7_枚举实现/Single;>;
  108. SourceFile: "Single.java"

image.png
如上反编译字节码文件得到,枚举默认继承extends java.lang.Enum类
image.png
该类阻止了默认的反序列化方法。