七种单例模式写法即介绍 缺乏、安全感 2022-11-18 08:04 154阅读 0赞 参考文章 [https://blog.csdn.net/mnb65482/article/details/80458571][https_blog.csdn.net_mnb65482_article_details_80458571] [https://blog.csdn.net/itachi85/article/details/50510124][https_blog.csdn.net_itachi85_article_details_50510124] # 定义 # 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例模式结构图: ![这里写图片描述][eb24aa2f420d0eb411af7df54da35e21.png] 首先我们要先了解下单例的四大原则: > 1.构造私有。 > 2.以静态方法或者枚举返回实例。 > > 3.确保实例只有一个,尤其是多线程环境。 > > 4.确保反序列换时不会重新构建对象。 我们常用的单例模式有: 饿汉模式、懒汉模式、双重锁懒汉模式、静态内部类模式、枚举模式,我们来逐一分析下这些模式的区别。 # 1. 饿汉模式 # public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){ } public static Singleton getInstance() { return instance; } } 饿汉模式在类被初始化时就已经在内存中创建了对象,**以空间换时间,基于类加载机制(静态对象,类初始化时JVM保证多线程安全性)避免了多线程的同步问题.** 这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。 这种方式基于类加载机制避免了多线程的同步问题,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到懒加载的效果。 # 2. 懒汉模式(线程不安全) # public class Singleton { private static Singleton instance; private Singleton (){ } public static Singleton getInstance() { //没加锁 if (instance == null) { //多个线程判断null,一起创建 instance = new Singleton(); } return instance; } } 懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,但**第一次加载时需要实例化,以时间换空间,在多线程环境下存在风险。** # 3. 懒汉模式(线程安全) # public class Singleton { private static Singleton instance; private Singleton (){ } public static synchronized Singleton getInstance() { //加了锁 if (instance == null) { instance = new Singleton(); } return instance; } } 这种写法能够在多线程中很好的工作,但是**每次调用getInstance方法时都需要进行同步,造成不必要的同步开销**,而且大部分时候我们是用不到同步的,所以不建议用这种模式。 # 4. 双重检查模式 (DCL)//这一步其实 instance = null # public class Singleton { private volatile static Singleton instance; private Singleton (){ } public static Singleton getInstance() { if (instance== null) { synchronized (Singleton.class) { if (instance== null) { instance= new Singleton(); } } } return singleton; } } **volatile** DCL模式的优点就是,只有在对象需要被使用时才创建,第一次判断 INSTANCE == null为了避免非必要加锁,当第一次加载时才对实例进行加锁再实例化。这样既可以节约内存空间,又可以保证线程安全。但是,由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况。具体分析如下: INSTANCE = new SingleTon(); 这个步骤,其实在jvm里面的执行分为三步: 1.在堆内存开辟内存空间。 2.在堆内存中实例化SingleTon里面的各个参数。 3.把对象指向堆内存空间。 由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。 不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,即在JDK1.6及以后,只要定义为private volatile static SingleTon INSTANCE = null;就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。 **if (instance== null)** 这种写法在getSingleton方法中对singleton进行了两次判空,**第一次是为了不必要的同步,第二次是在singleton等于null的情况下才创建实例。** **DCL优点是资源利用率高,第一次执行getInstance时单例对象才被实例化,效率高。缺点是第一次加载时反应稍慢一些,在《java并发编程实践》一书建议用静态内部类单例模式来替代DCL。** # 5. 静态内部类单例模式 # public class Singleton { private Singleton(){ } public static Singleton getInstance(){ return SingletonHolder.sInstance; } private static class SingletonHolder { private static final Singleton sInstance = new Singleton(); } } **简单总结** 第一次加载外部类Singleton时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载内部类SingletonHolder 并初始化sInstance . 需要注意的是getInstance并没有去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建. 加载内部类并初始化instance这一步, `private static SingleTon INSTANCE = new SingleTon()`,静态成员变量INSTANCE 在类加载的时候就把对象创建了,这一步的线程安全是JVM底层保证的.因此没有线程安全问题. public class SingleTon{ private SingleTon(){ } private static class SingleTonHoler{ private static SingleTon INSTANCE = new SingleTon(); } public static SingleTon getInstance(){ return SingleTonHoler.INSTANCE; } } **详细介绍** 静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE\*\*,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性\*\*,同时也延迟了单例的实例化。 **静态内部类实现线程安全** 那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。 类加载时机: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的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。 我们再回头看下getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,**getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建**。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。 **静态对象创建过程中保证线程安全** 那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话: 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。 故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。 那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。 # 6. 枚举单例 # public enum Singleton { INSTANCE; public void doSomeThing() { } } 默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法: private Object readResolve() throws ObjectStreamException{ return singleton; } 枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。 # 7. 使用容器实现单例模式 # public class SingletonManager { private static Map<String, Object> objMap = new HashMap<String,Object>(); private Singleton() { } public static void registerService(String key, Objectinstance) { if (!objMap.containsKey(key) ) { objMap.put(key, instance) ; } } public static ObjectgetService(String key) { return objMap.get(key) ; } } 用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。 用SingletonManager 将多种的单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。 # 总结 # 到这里七中写法都介绍完了,至于选择用哪种形式的单例模式,取决于你的项目本身,是否是有复杂的并发环境,还是需要控制单例对象的资源消耗。 [https_blog.csdn.net_mnb65482_article_details_80458571]: https://blog.csdn.net/mnb65482/article/details/80458571 [https_blog.csdn.net_itachi85_article_details_50510124]: https://blog.csdn.net/itachi85/article/details/50510124 [eb24aa2f420d0eb411af7df54da35e21.png]: /images/20221022/5dd913e5ee954bd7bc840db09bf7864c.png
还没有评论,来说两句吧...