当前位置:   article > 正文

深入理解单例模式_onestar博客

onestar博客

目录

【1】方法一:饿汉单例

【2】方法二:懒汉单例

【3】方法三:懒汉单例加强(同步方法)

【4】方法四:懒汉单例加强(DCL)

【5】方法五:DCL加强

【6】方法六:懒汉单例加强(静态内部类)

【6】方法七:枚举单例(最完美写法)

总结


 

单例模式有八种写法,不是说设计模式是代表了最佳的实践吗,这一下冒出八种写法,何谈最佳?

每一种单例的写法基本上都可以破坏其单例的属性,这就带来了安全隐患,所以每一种写法都是在之前的基础上进行加强,但是比消此涨,这会增加空间复杂度或者时间复杂度,没有最优的用法,只有最合适的用法。

这八种里面有些方法是重复的,只是写法稍变一下,重复的就不讲解了,每种方法通过多线程并发进行测试,通过构造方法执行的次数查看创建对象的数量

【1】方法一:饿汉单例

饿汉单例,顾名思义,饿汉就是比饿,比较饥渴,比较冲动,一上来就进入主题,创建对象。

  • 在类加载时就创建了单例对象
  • 只创建一个实例,绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题,JVM保证线程安全
  • 优点:没有加任何锁,执行效率高,线程安全
  • 缺点:不管你是否用到,类装载的时候就完成实例化,浪费内存空间(空间换时间)
  1. public class Hungry {
  2.     // 私有构造器
  3.     private Hungry(){
  4.         System.out.println(Thread.currentThread().getName());
  5.     }
  6.     // 指向自己实例的私有静态引用
  7.     private static final Hungry hungry = new Hungry();
  8.     // 以自己实例为返回值的静态的公有方法
  9.     public static Hungry getInstance(){
  10.         return hungry;
  11.     }
  12. }
  13. // 多线程并发测试
  14. @Test
  15. public void TestMain1(){
  16.     for (int i = 0;i < 10;i++){
  17.         new Thread(() -> {
  18.             Hungry.getInstance();
  19.         }).start();
  20.     }
  21. }

image-20210205155802668

【2】方法二:懒汉单例

懒汉模式,就是比较懒,做事比较拖拉,在调用实例方法的时候才去实例化对象,一开始我就不给你 new 对象,你来找我,我再给你创建一个对象

  • 在调用实例方法的时候才去实例化对象
  • getInstance方法中线程不安全,会创建了多个实例
  • 优点:一直没人用的话,就不会创建实例,节约内存空间
  • 缺点:浪费判断时间,降低效率(时间换空间),线程不安全
  1. public class LazyMan {
  2.     // 私有构造器
  3.     private LazyMan(){
  4.         System.out.println(Thread.currentThread().getName());
  5.     }
  6.     // 指向自己实例的私有静态引用
  7.     private static LazyMan lazyMan = null;
  8.     // 以自己实例为返回值的静态的公有方法
  9.     public static LazyMan getInstance(){
  10.         if(lazyMan == null){
  11.             lazyMan = new LazyMan();
  12.         }
  13.         return lazyMan;
  14.     }
  15. }
  16. // 多线程并发测试
  17. @Test
  18. public void TestMain2(){
  19.     for (int i = 0;i < 10;i++){
  20.         new Thread(() -> {
  21.             LazyMan.getInstance();
  22.         }).start();
  23.     }
  24. }

image-20210205165553057

【3】方法三:懒汉单例加强(同步方法)

方法二存在线程不安全的隐患,我们可以通过加锁来进行加强,方法声明上加上 synchronized,同步方法

  • 和方法二一样,在调用实例方法的时候才去实例化对象
  • 在方法声明上加锁,保证线程安全
  • 优点:一直没人用的话,就不会创建实例,节约内存空间,且线程安全
  • 缺点:每次创建实例都要判断锁,浪费判断时间,降低效率(时间换空间)
  1. public class LazyManSyn {
  2.     // 私有构造器
  3.     private LazyManSyn(){}
  4.     // 私有静态引用创建对象
  5.     private static LazyManSyn lazyManSyn = null;
  6.     // 同步方法创建实例对象
  7.     public static synchronized LazyManSyn getInstance(){
  8.         if (lazyManSyn == null){
  9.             lazyManSyn = new LazyManSyn();
  10.         }
  11.         return lazyManSyn;
  12.     }
  13. }
  14. // 多线程并发测试
  15. @Test
  16. public void TestMain3(){
  17.     for (int i = 0; i < 10; i++) {
  18.         new Thread(()->{
  19.             System.out.println(LazyManSyn.getInstance().hashCode());
  20.         }).start();
  21.     }
  22. }

 

image-20210206103944913

【4】方法四:懒汉单例加强(DCL)

方法三虽然线程安全,但每次创建实例都需要判断锁,效率低,我们可以通过双重锁来进行加强,让已经有实例对象的时候不进行锁判断,这种方式称之为DCL懒汉式

  • 和方法二一样,在调用实例方法的时候才去实例化对象
  • 通过双重锁来防止多实例(双重判断和锁)
  • 第一个 if 判断:提高执行效率,如果对象不为空,就直接不执行下面的程序
  • 第二个 if 判断:在第一个 if 判断和 synchronized 锁之间,可以进来多个线程,如果没有第二个 if 判断,一个线程拿到锁 new 对象后释放锁,第二个线程又能够拿到锁 new 对象,通过第二个 if 则可以避免创建多个对象。
  • synchronized 锁:通过同步代码块
  • 优点:减少了锁的判断,相对于方法三提升了效率,使用双重锁,暂且认为线程安全(后面再说)
  • 缺点:其实这个方法还是存在线程不安全的隐患,在lazyManDCL = new LazyManDCL();创建对象的时候,不是一个原子性操作,会出现指令重排的可能,因此也可能创建多个实例
  1. public class LazyManDCL {
  2.     // 私有构造器
  3.     private LazyManDCL(){}
  4.     // 指向自己实例的私有静态引用
  5.     private static LazyManDCL lazyManDCL = null;
  6.     // 双重检查锁模式
  7.     public static LazyManDCL getInstance(){
  8.         if(lazyManDCL == null){
  9.             synchronized (LazyManDCL.class){
  10.                 if(lazyManDCL == null){
  11.                     lazyManDCL = new LazyManDCL();
  12.                 }
  13.             }
  14.         }
  15.         return lazyManDCL;
  16.     }
  17. }
  18. // 多线程并发测试
  19. @Test
  20. public void TestMain3(){
  21.     for (int i = 0;i < 10;i++){
  22.         new Thread(() -> {
  23.             System.out.println(LazyManDCL.getInstance().hashCode());
  24.         }).start();
  25.     }
  26. }

image-20210206103944913

【5】方法五:DCL加强

在DCL单例模式中,虽然能够通过双重锁保证一定的线程安全性,但是在 new 对象的时候,非原子性操作造成指令重排,执行 new LazyManDCL(); 的过程如下:

  1. 分配内存空间
  2. 执行构造方法
  3. 把这个对象指向这个空间

执行代码时,为了提高性能,编译器和处理器往往会对指令进行重排序,上面的执行顺序可能是 1—>2—>3,也可能是 1—>3—>2,当执行顺序为 1—>3—>2 时,就会出现如下问题:

  • A线程执行 1—>3 ,分配了内存空间,把这个对象指向这个空间
  • 此时B线程进来,由于A线程指向了这个空间,造成第一个 if 判断为 false,从而直接 return 对象,由于没有初始化对象,就会报错

因此我们要防止指令重排的现象发生,即:使用 volatile 关键字,代码如下,就是在方法四的基础上加了一个 volatile 关键字

  • 优点:线程安全
  • 缺点:多重判断,浪费时间,降低效率
  1. public class LazyManDCL {
  2.     // 私有构造器
  3.     private LazyManDCL(){}
  4.     // 指向自己实例的私有静态引用,使用volatile防止指令重排
  5.     private static volatile LazyManDCL lazyManDCL = null;
  6.     // 双重检查锁模式
  7.     public static LazyManDCL getInstance(){
  8.         if(lazyManDCL == null){
  9.             synchronized (LazyManDCL.class){
  10.                 if(lazyManDCL == null){
  11.                     lazyManDCL = new LazyManDCL();
  12.                 }
  13.             }
  14.         }
  15.         return lazyManDCL;
  16.     }
  17. }
  18. // 多线程并发测试
  19. @Test
  20. public void TestMain4(){
  21.     for (int i = 0;i < 10;i++){
  22.         new Thread(() -> {
  23.             System.out.println(LazyManDCL.getInstance().hashCode());
  24.         }).start();
  25.     }
  26. }

image-20210206115800861

【6】方法六:懒汉单例加强(静态内部类)

通过静态内部类来创建实例,静态内部类单例模式的核心原理为对于一个类,JVM在仅用一个类加载器加载它时,静态变量的赋值在全局只会执行一次!,

  • 优点1:外部类加载时,不会立即加载内部类,因此不占内存
  • 优点2:不像DCL那样需要进行多重判断,提升了效率
  • 优点3:第一次调用getInstance()方法时,虚拟机才加载SingleOne类,既能保证线程安全,又能保证实例的唯一性,同时也延迟了单例的实例化
  1. public class SingleOne {
  2.     // 私有构造方法
  3.     private SingleOne(){}
  4.     // 通过静态内部类创建实例
  5.     private static class InnerClass{
  6.         private static final SingleOne SINGLE_ONE = new SingleOne();
  7.     }
  8.     // 通过内部类返回对象实例
  9.     public static SingleOne getInstance(){
  10.         return InnerClass.SINGLE_ONE;
  11.     }
  12. }
  13. // 多线程并发测试
  14. @Test
  15. public void TestMain5(){
  16.     for (int i = 0; i < 10; i++) {
  17.         new Thread(()->{
  18.             System.out.println(SingleOne.getInstance().hashCode());
  19.         }).start();
  20.     }
  21. }

image-20210206185449387

【6】方法七:枚举单例(最完美写法)

上面的方法都是在忽略反射的情况下,我们都知道,反射可以破坏类,能够无视私有构造器,因此,上面的单例都可以使用反射进行破坏,为了解决反射破坏,我们可以使用枚举单例。

  • 枚举的特性本身就是单例,在任何情况下都是一个单例
  • 直接通过EnumSingle.INVALID进行调用
  • 让JVM来帮我们保证线程安全和单一实例问题
  1. public enum EnumSingle {
  2.     INVALID;
  3.     public void doSomething(){}
  4. }
  5. // 多线程并发测试
  6. @Test
  7. public void TestMain6(){
  8.     for (int i = 0; i < 10; i++) {
  9.         new Thread(()->{
  10.             System.out.println(EnumSingle.INVALID.hashCode());
  11.         }).start();
  12.     }
  13. }

image-20210206204120457

总结

单例模式写法有很多种,稍微改动一下可能又是一种,不过最完美的还是方法七的枚举单例,但是用的最多的还是第一种,因为简单,易于理解,更适合开发者。其实我们没有必要拘泥于完美,最合适的才是最好的,用什么方式解决实际问题更合适就用什么方式,不要追求那些不必要的完美。就像两个人在一起,可能他(她)足够完美,你很喜欢,然而却因为种种原因不是那么合适,喜欢是乍见之欢,久处仍怦然,合适是你来我往,听得懂和聊得来,你是用心动的方法还是用自己能够信手拈来的方法呢?当然,即喜欢又合适是最好的,无论多难,你也总会遇到即喜欢有合适的,毕竟地球是圆的,只是时间早晚的问题!

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

闽ICP备14008679号