当前位置:   article > 正文

单例模式的5种实现方式,最后一种最推荐_单例模式的几种实现方式

单例模式的几种实现方式

单例模式(Singleton Pattern)是一种常用的软件设计模式,它保证一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下都非常有用,例如配置管理、日志记录等。在本文中,我们将介绍几种实现单例模式的方法,并给出相应的代码示例。

1. 饿汉式(Eager Initialization)

饿汉式是最简单的实现方式,它在类加载时就创建了一个实例。这种方式的优点是线程安全,因为实例在类加载时就已经被创建,不可能存在多个实例。但是,这种方式的缺点是如果这个实例在程序运行过程中没有被使用到,那么它会一直占用内存空间。

public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2. 懒汉式(Lazy Initialization)

懒汉式是在第一次调用 getInstance() 方法时才创建实例。这种方式的优点是节省了内存空间,因为只有在需要时才会创建实例。但是,这种方式的缺点是线程不安全,可能会创建多个实例。为了解决这个问题,我们可以使用同步方法(Synchronized Method)或者双重检查锁定(Double-Checked Locking)。

2.1 强制同步

这种方法的虽然可以保证实例唯一,但是因每次调用getInstance()时都需要同步,会造成性能浪费。

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2.2 双重检查锁定

相对第一种方式,更加高效和安全。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3. 静态内部类(Static Inner Class)

静态内部类的方式是在内部类中创建实例,这样可以实现懒加载,同时保证了线程安全。这种方式的优点是避免了同步方法的性能问题。

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

知识点补充 :

JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。

  1. 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
  3. 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
  5. 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。 只有SingletonObject的 getInstance()方法被调用的时候,InstanceHolder才会去初始化instance,静态对象instace才被创建,相当于饿汉式单例。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

故而可以看出,INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

4. 枚举(Enum)

枚举的方式是通过枚举类型来实现单例模式,这种方式的优点是简单且线程安全。

  1. 首先是要使用的类。

    public class MySingleton {}
    }
    
    • 1
    • 2
  2. 类对应的枚举。

    public enum MySingletonEnum {
        SINGLETON;
        private MySingleton mySingleton = null;
        private MySingletonEnum() {
            mySingleton = new MySingleton();
        }
        public MySingleton getMySingleton() {
            return mySingleton;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    将此Enum的class文件,执行javap反编译后得到:

    	public final class cn.zdy.design_pattern.singleton.MySingletonEnum extends java.lang.Enum<cn.zdy.design_pattern.singleton.MySingletonEnum> {
      public static final cn.zdy.design_pattern.singleton.MySingletonEnum SINGLETON;
    
      public static cn.zdy.design_pattern.singleton.MySingletonEnum[] values();
        Code:
           0: getstatic     #7                  // Field $VALUES:[Lcn/zdy/design_pattern/singleton/MySingletonEnum;
           3: invokevirtual #11                 // Method "[Lcn/zdy/design_pattern/singleton/MySingletonEnum;".clone:()Ljava/lang/Object;
           6: checkcast     #12                 // class "[Lcn/zdy/design_pattern/singleton/MySingletonEnum;"
           9: areturn
    
      public static cn.zdy.design_pattern.singleton.MySingletonEnum valueOf(java.lang.String);
        Code:
           0: ldc           #1                  // class cn/zdy/design_pattern/singleton/MySingletonEnum
           2: aload_0
           3: invokestatic  #16                 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
           6: checkcast     #1                  // class cn/zdy/design_pattern/singleton/MySingletonEnum
           9: areturn
    
      public cn.zdy.design_pattern.singleton.MySingleton getMySingleton();
        Code:
           0: aload_0
           1: getfield      #26                 // Field mySingleton:Lcn/zdy/design_pattern/singleton/MySingleton;
           4: areturn
    
      static {};
        Code:
           0: new           #1                  // class cn/zdy/design_pattern/singleton/MySingletonEnum
           3: dup
           4: ldc           #35                 // String SINGLETON
           6: iconst_0
           7: invokespecial #36                 // Method "<init>":(Ljava/lang/String;I)V
          10: putstatic     #3                  // Field SINGLETON:Lcn/zdy/design_pattern/singleton/MySingletonEnum;
          13: invokestatic  #37                 // Method $values:()[Lcn/zdy/design_pattern/singleton/MySingletonEnum;
          16: putstatic     #7                  // Field $VALUES:[Lcn/zdy/design_pattern/singleton/MySingletonEnum;
          19: return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
  3. 测试类

    public class MySingleTonTest {
        public static void main(String[] args) {
            MySingleton con1 = MySingletonEnum.SINGLETON.getMySingleton();
            MySingleton con2 = MySingletonEnum.SINGLETON.getMySingleton();
            System.out.println(con1 == con2);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    结果是true,说明确实是单例。

知识点补充

由反编译后的代码可知,SINGLETON 被声明为 static,根据类加载过程可知,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步。<clinit>()方法是由编译器自动收集类中的所有类变量的复制动作和静态语句块中的语句合并产生,编译器收集的顺序是由语句在源文件在出现的顺序锁具顶,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他后的变量可以赋值。但不能访问。

所以,枚举实现是在实例化时是线程安全。

接下来看看序列化问题:

Java规范中规定,每一个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
也就是说,以下面枚举为例,序列化的时候只将 SINGLETON 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

总结

单例模式有多种实现方式,选择哪种方式取决于你的具体需求。如果你希望在程序启动时就创建实例,可以选择饿汉式;如果你希望在需要时才创建实例,可以选择懒汉式、静态内部类或枚举。在多线程环境下,需要注意线程安全问题。 希望本文能帮助你更好地理解和使用单例模式。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/729623
推荐阅读
相关标签
  

闽ICP备14008679号