单例模式之饿汉式与懒汉式

常见的单例模式

本文仅讲解单例模式中的饿汉式和懒汉式(双检索)

文章目录

前言

先来简单的说说什么是单例模式。所谓单例就是在系统中只有一个该类的实例,或者说类只能被创建一次。单例模式(Singleton),也是23种设计模式中常见的一种。

一、单例模式的优缺点

优点:
1、在单例模式中,单例对象的类必须保证只有一个实例存在,这就说明这种模式能一定程度的减少内存开支。
2、配置的读取,现在市场上较为常见的场景,将配置信息放在某个配置文件中,这些配置数据由一个单例对象统一读取。采用单例模式,即可减少系统的性能开销。
缺点:
1、单例模式既只能被实例化一次,没有抽象层,那么它想要扩展,就没有其他的途径(代码无bug的情况下,下文会提到)
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。

二、详解懒汉式、饿汉式

1.饿汉式

顾名思义,饿汉式,饿极了的汉子,有饭就吃,不会规规矩矩的等到饭点才去食堂,所以在加载初始化的时候就已经被创建出实例了。

先上代码(饿汉式):

import java.io.Serializable;

/** * @ClassName Singleton * @Descption TODO * @ClassName lk * Version 1.0 **/
public class Singleton { 

    private static final Singleton INST = new Singleton();
    
    //构造方法
    private Singleton(){ 
        System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
    }

    public static Singleton getInstance(){ 
        return INST;
    }
    //我用来测试
    public static void testfun(){ 
        System.out.println("我是用来debug测试饿汉式的方法");
    }
}

饿汉式测试

public class EhanTest { 

    public static void main(String[] args) { 
        Singleton.testfun();
        //可以根据输出的对象判断是否是同一实例
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
    }
}

读完了代码,那么上述代码是否真的只能被创建一次呢?
其实不然,接着往下看:
(1)、反射机制

        Class<?> clazz = Singleton.class;
        //根据类型获取类的构造方法
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        //让私有的构造方法可以被使用
        constructor.setAccessible(true);
        //创建个新对象
        constructor.newInstance();

既然根据反射机制可以破坏我的单例模式,那我能否预防这种场景发生呢?当然可以

import java.io.Serializable;

/** * @ClassName Singleton * @Descption TODO * @ClassName lk * Version 1.0 **/
public class Singleton { 

    private static final Singleton INST = new Singleton();
    
    //构造方法
    private Singleton(){ 
    if (INST != null){ 
            throw new RuntimeException("我已心有所属,不能再次被创建");
        }
        System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
    }

    public static Singleton getInstance(){ 
        return INST;
    }
    //我用来测试
    public static void testfun(){ 
        System.out.println("我是用来debug测试饿汉式的方法");
    }
}

在构造方法中直接判断是否已经被创建即可
(2)、反序列化
这种场景需要你的单例模式实现序列化接口,还是较为常见

        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ObjectOutputStream obj = new ObjectOutputStream(os);
        //对象转换为字节流
        obj.write(singleton);
        ObjectInputStream bs = new ObjectInputStream(new ByteArrayInputStream(os.toByteArray()));
        //还原对象
        bs.readObject();
  • 参考第一种反射机制,这种场景是否在构造方法中加入判断即可预防?实际上反序列化场景是不会走构造方法的
public Object readResolve(){ 
        return INST;
    }
  • 预防这种情况加入readResolve()方法即可,原理比较麻烦,之后会写篇专门的叙述。

3、Unsafe()方法
unsafe是JDK提供的类,可以根据类型来创建实例,并且和反序列化相同,都是不走构造方法,这种场景目前不知道有什么方法可以预防,有了解的大神可以评论下,我也学习学习,哈哈哈

2.懒汉式(双检锁)

  • 懒汉式,很懒的抠脚大汉,懒的一动不动,只有需要他的时候才会有所行动,所以不被调用,那么他就不会被创建。

懒汉两式之第一式:

import java.io.Serializable;

/** * @ClassName Singleton * @Descption TODO * @ClassName lk * Version 1.0 **/
public class Singleton implements Serializable { 

    private static Singleton INST = null;

    //构造方法
    private Singleton(){ 
        System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
    }

    public static Singleton getInstance(){ 
        if (INST == null){ 
            INST = new Singleton();
        }
        return INST;
    }

    //我用来测试
    public static void testfun(){ 
        System.out.println("我是用来debug测试懒汉式的方法");
    }
}

上述饿汉式中可以被破坏,从而创建新的对象,那么懒汉式会不会也会被创建多个多个实例,从而破坏单例模式。

   public static Singleton getInstance(){ 
        if (INST == null){ // 1 判断
            INST = new Singleton();//2 创建对象
        }
        return INST;
    }
  • 在多线程环境下,线程1调用getInstance()方法执行到 2 创建对象还没结束时,线程2同样进入1判断是否为null,由于线程1
    创建对象的操作还未完成,线程2同样走上了创建对象的道路,最终导致创建多个对象,单例又被破坏掉。此时,我们可以用加锁来解决
    public static synchronized Singleton getInstance(){ 
        if (INST == null){ 
            INST = new Singleton();
        }
        return INST;
    }
  • 使用synchronized固然可以解决此问题,但引发了另外一个问题,只有第一次用到才起到关键作用,达到你想要的效果,但是创建了实例之后的操作就显得画蛇添足->加锁影响到性能,所以书山有路勤为径,学海无涯苦作舟,还得接着学呀!

重点来了:懒汉式(双检索)

这种实际就是对懒汉式做了优化,即在获取锁的之前加了判断


import java.io.Serializable;

/** * @ClassName Singleton * @Descption TODO * Version 1.0 **/
public class Singleton implements Serializable { 

    // 此处的volatile不可忽略
    private static volatile Singleton INST = null;

    //构造方法
    private Singleton(){ 
        System.out.println("我是个私有的静态方法,目的是不被随便调用,从而创建实例对象");
    }

    public static Singleton getInstance(){ 
        if (INST == null){ 
            synchronized(Singleton.class){ 
                if (null==INST){ 
                    INST = new Singleton();
                }
            }
        }
        return INST;
    }

    //我用来测试
    public static void testfun(){ 
        System.out.println("我是用来debug测试懒汉式的方法");
    }
}

下面对双检索详细讲解

 public static Singleton getInstance(){ 
        if (INST == null){ // 1 判断
            synchronized(Singleton.class){  //2 获取锁
                if (null==INST){  // 3 判断
                    INST = new Singleton(); // 创建对象
                }
            }
        }
        return INST;
    }
  • 对于 1 判断,较为容易理解,判断是否为null,解决竞争问题,不会有性能上的损失,那么既然之前有了判断,为什么3处又加了判断?
  • 同样的多线程环境,如果如果没有3 判断,并且线程1和线程2同事执行到2 竞争锁的这一步,某个线程抢到资源进入4
    创建对象时,创建对象完成,释放锁,此时,另一个线程抢到所资源,进入创建对象的过程,结局不言而喻,又是创建了多个对象,单例被破坏,又GG了,所以是两处判断检查,双检索的懒汉式。

代码中一个关键词,不知是否有注意到:volatile
volatile 有两个作用:

  • 1、有序性
  • 2、可见性

上述代码中volatile 的作用用到了有序性,防止指令重排序。又来接着学吧!
众所周知,创建对象总共分几步?三步!

  • 第一步:冰箱门打开
  • 第二步:把对象放进去
  • 第三步:把冰箱门关上

哈哈哈哈哈,实际差不多,这样更好理解!

  • 1、需要分配内存空间
  • 2、调用构造方法(成员变量的初始化赋值)
  • 3、静态变量的赋值

问题来了:

  • 创建对象这三步,除了第一步分配内存空间,其他两个步骤并没有必然的先后顺序,那么CPU在执行的时候,可能会对这两步骤进行先后顺序的调整,这样在多线程环境下会引发一系列的问题。
 public static Singleton getInstance(){ 
        if (INST == null){ // 1 判断
            synchronized(Singleton.class){  //2 获取锁
                if (null==INST){  // 3 判断
                    INST = new Singleton(); // 创建对象
                }
            }
        }
        return INST;
    }

如果创建对象的三步骤经过CPU排序之后最终执行的顺序是
1、需要分配内存空间
2、静态变量的赋值
3、调用构造方法(成员变量的初始化赋值)

  • 那么多线程环境下,线程1正在创建对象,刚好执行完2静态变量的赋值,注意,此时静态变量不为null,线程2执行到了1 判断的时候,直接跳过了锁资源这一步,直接返回,构造方法还未执行,拿到的实例有啥用呢?乱七八糟的问题随之而来。

  • 但是当静态变量有了volatile之后,便不会有这种场景,因为volatile会在静态变量赋值语句之后加入内存屏障,阻止了CPU指令排序,让构造方法这步操作越过屏障,在静态变量赋值之后执行,简单的来讲,它的作用就是可以让静态变量在最后一步才执行。

总结

  • 作为技术型文章,没有华丽的语言和生动的文字,有的仅仅是数行代码和对其枯燥的描述,希望本文能对即将开始写作的你,有所帮助。

  • 本文为作者原创文章,未经原创作者同意不得转载

    原文作者:你可以永远相信暴龙战士
    原文地址: https://blog.csdn.net/qq_41105145/article/details/123911228
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞