• 单例设计模式


    1. //饿汉式单例模式
    2. public class SingletonTest01 {
    3. public static void main(String[] args) {
    4. Singleton instance = Singleton.getInstance();
    5. Singleton instance1 = Singleton.getInstance();
    6. System.out.println(instance == instance1);
    7. }
    8. }
    9. class Singleton {
    10. //私有化构造函数
    11. private Singleton(){}
    12. //实例化一个静态变量
    13. private static final Singleton SINGLETON = new Singleton();
    14. //外部只能通过这个方法获取对象,并且每次只会获取同一个
    15. public static Singleton getInstance() {
    16. return SINGLETON;
    17. }
    18. }

    饿汉式(静态变量

    优缺点:

    优点:这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步,因为clinit方法时多线程同步枷锁的。

    缺点:再类装载的时候就完成实例化,没有达到Lazy Loading的效果,如果从开始至终从未使用过这个实例,则会造成内存的浪费

    这种方式基于classLoader机制避免了线程同步的问题。不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类状态的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类状态,这时候初始化instance就没有达到lazy loading的效果

    结论:这儿单例模式可用,可能会造成内存浪费


    饿汉式(静态代码块)

    1. //饿汉式单例模式
    2. public class SingletonTest02 {
    3. public static void main(String[] args) {
    4. Singleton instance = Singleton.getInstance();
    5. Singleton instance1 = Singleton.getInstance();
    6. System.out.println(instance == instance1);
    7. }
    8. }
    9. class Singleton {
    10. //私有化构造函数
    11. private Singleton(){ }
    12. //本类内部创建对象实例
    13. private static Singleton SINGLETON;
    14. //在静态代码块中,创建单例对象
    15. static {
    16. SINGLETON = new Singleton();
    17. }
    18. //外部只能通过这个方法获取对象,并且每次只会获取同一个
    19. public static Singleton getInstance() {
    20. return SINGLETON;
    21. }
    22. }

    优缺点:

    这种方式和上面的方式类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例,优缺点也和上面一样

    结论:这种单例模式可用,但是可能造成内存浪费


    懒汉式(线程不安全)

    1. //懒汉式单例模式(线程不安全)
    2. public class SingletonTest03 {
    3. public static void main(String[] args) {
    4. Thread t1 = new Thread(() -> {
    5. Singleton instance = Singleton.getInstance();
    6. System.out.println("线程1:" + instance.hashCode());
    7. });
    8. Thread t2 = new Thread(() -> {
    9. Singleton instance = Singleton.getInstance();
    10. System.out.println("线程2:" + instance.hashCode());
    11. });
    12. t1.start();
    13. t2.start();
    14. }
    15. }
    16. class Singleton {
    17. private static Singleton SINGLETON = null;
    18. //私有化构造函数
    19. private Singleton(){ }
    20. //提供一个静态的公有方法,当使用改方法时,才去创建instance
    21. public static Singleton getInstance() {
    22. if (SINGLETON == null) {
    23. SINGLETON = new Singleton();
    24. }
    25. return SINGLETON;
    26. }
    27. }

    多次执行main方法后,会发向两次获取对象的hashcode不一样

     

    优缺点:

    • 起到了lazy loading的效果,但是只能在单线程下使用
    • 如果在多线程下,一个线程进入了if(SINGLETON == nul)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,所以在多线程环境下不可能使用这种方式
    • 结论:在实际开发中,不要使用这种方式

    懒汉式(线程安全,同步方法) 

    1. //懒汉式单例模式(线程安全,同步方法)
    2. public class SingletonTest03 {
    3. public static void main(String[] args) {
    4. Thread t1 = new Thread(() -> {
    5. Singleton instance = Singleton.getInstance();
    6. System.out.println("线程1:" + instance.hashCode());
    7. });
    8. Thread t2 = new Thread(() -> {
    9. Singleton instance = Singleton.getInstance();
    10. System.out.println("线程2:" + instance.hashCode());
    11. });
    12. t1.start();
    13. t2.start();
    14. }
    15. }
    16. class Singleton {
    17. private static Singleton SINGLETON = null;
    18. //私有化构造函数
    19. private Singleton(){ }
    20. //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
    21. public static synchronized Singleton getInstance() {
    22. if (SINGLETON == null) {
    23. SINGLETON = new Singleton();
    24. }
    25. return SINGLETON;
    26. }
    27. }

    优缺点

    • 解决了线程不安全问题
    • 效率太低了,每个线程在想获得类的实例的时候,执行getInstance()方法都要进行同步,而这个方法只执行一次实例化代码就够了,后面想获得该类实例,直接return就行了,方法进行同步效率太低
    • 结论:在实际开发中,不推荐使用这种方式

    懒汉式(双重检查DoubleCheck)

    1. //懒汉式单例模式(线程安全,双重检查)
    2. public class SingletonTest03 {
    3. public static void main(String[] args) {
    4. Thread t1 = new Thread(() -> {
    5. Singleton instance = Singleton.getInstance();
    6. System.out.println("线程1:" + instance.hashCode());
    7. });
    8. Thread t2 = new Thread(() -> {
    9. Singleton instance = Singleton.getInstance();
    10. System.out.println("线程2:" + instance.hashCode());
    11. });
    12. t1.start();
    13. t2.start();
    14. }
    15. }
    16. class Singleton {
    17. private static volatile Singleton SINGLETON = null;
    18. //私有化构造函数
    19. private Singleton(){ }
    20. //提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题,保证效率
    21. public static Singleton getInstance() {
    22. if (SINGLETON == null) {
    23. synchronized (Singleton.class){
    24. if (SINGLETON == null){
    25. SINGLETON = new Singleton();
    26. }
    27. }
    28. }
    29. return SINGLETON;
    30. }
    31. }

     优缺点:

    DoubleCheck是多线程开发中常用到的,如代码中所示,我们进行了两次if(SINGLETON == null)检查,这样就可以保证线程安全了

    这样,实例化代码只用执行一次,后面再次访问的时候,判断if(SINGLETON == null),直接return实例化对象,也避免了反复进行方法同步

    线程安全,延迟加载,效率较高

    结论:实际开发中,推荐使用这种单例设计模式

    关于SINGLETON被volatile修饰,参考了原文链接:https://blog.csdn.net/awake_lqh/article/details/106276859

    volatile关键字的作用其实就是让该变量的变化对于每一个线程可见,其底层实现原理是由于java内存模型(jmm)中的封装了8个交互操作。

    read:把一个主内存中的值传递到工作内存,以便load动作使用
    load:把read操作从主内存获取的内存变量赋值到工作内存的变量副本
    use:将工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的字节码指令的时候将会执行这个操作。
    assign:从执行引擎接受到的值赋给工作内存的变量,当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    store:他把工作内存中的一个变量的值传送到主内存中,以便随后的write操作使用
    write:把store操作从工作内存中得到的变量的值放入主内存的变量中
    lock:作用于主内存的变量,把一个变量标示一条线程独占的状态。
    Unlock:作用于主内存,把一个处于锁定状态的变量释放出来。释放后的变量才可以被其他线程锁定。
    每次执行use操作的时候都先执行read和load操作,让volatile修饰的变量每次获取的都是新的值;

    每次执行assign的时候,随后都会执行store和write操作,让volatile修饰的变量每次都刷新到主内存中。

    还有一个点就是其禁止指令重排序。

    uniqueInstance = new Singleton();

    主要在于uniqueInstance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

    1. 给 uniqueInstance 分配内存
      2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
      3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null了)

    在JVM的即时编译器中存在指令重排序的优化。
    ​ 也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错(因为没有初始化)  
    ​ 再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance ==null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。这里的关键在于线程T1对instance的写操作没有完成,线程T2就执行了读操作。

    volatile如何解决
    ​ volatile关键字的一个作用是禁止指令重排,把uniqueInstance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不用会调用读操作。

    何为内存屏障
    观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,此lock非jmm交互操作的lock。

    lock指令的作用是使本cpu的cache写入内存,该写入动作会引起其他cpu的cache无效化(缓存一致性)。通过这样一个操作让对于volatile变量的修改对于其他cpu可变。

    lock指令把之前的cache都同步到内存中,等同于让lock指令后面的指令依赖于lock指令前面的指令,根据处理器在进行重排序时是会考虑指令之间的数据依赖性,所以lock指令之前的指令不会跑到lock指令之后,之后的也不会跑到之前。

    so
    volatitle解决了两个问题:instance的线程可见性、以及在初始化instance的时候遇到的指令重排序问题。

    double check的意义
    为什么要判断两次instance==null呢???

    第一次检测:

    ​ 由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,每次都要去竞争锁。

    第二次检测:

    ​ 如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

    ​ 简单来说就是为了防止创建多个实例。
     


    懒汉式(静态内部类)

    1. //懒汉式单例模式(静态内部类)
    2. public class SingletonTest04 {
    3. public static void main(String[] args) {
    4. Thread t1 = new Thread(() -> {
    5. Singleton instance = Singleton.getInstance();
    6. System.out.println("线程1:" + instance.hashCode());
    7. });
    8. Thread t2 = new Thread(() -> {
    9. Singleton instance = Singleton.getInstance();
    10. System.out.println("线程2:" + instance.hashCode());
    11. });
    12. t1.start();
    13. t2.start();
    14. }
    15. }
    16. //静态内部类完成
    17. class Singleton {
    18. //私有化构造函数
    19. private Singleton(){ }
    20. //写一个静态内部类,该类中有一个静态属性SINGLETON
    21. private static class SingletonInstance {
    22. private static final Singleton INSTANCE = new Singleton();
    23. }
    24. //提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
    25. public static Singleton getInstance() {
    26. return SingletonInstance.INSTANCE;
    27. }
    28. }

    优缺点:

    • 这种方式采用了类装载的机制来保证初始化实例时只会一个线程进行
    • 静态内部类方式在Singleton类被装载时不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化
    • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程无法进入
    • 优点:避免了线程不安全的问题,利用静态内部类特点实现延迟加载,效率高
    • 结论:推荐使用

    单例模式(枚举)

     

    1. public class SingletonTest05 {
    2. public static void main(String[] args) {
    3. Thread t1 = new Thread(() -> {
    4. Singleton instance = Singleton.INSTANCE;
    5. System.out.println("线程1:" + instance.hashCode());
    6. });
    7. Thread t2 = new Thread(() -> {
    8. Singleton instance = Singleton.INSTANCE;
    9. System.out.println("线程2:" + instance.hashCode());
    10. });
    11. t1.start();
    12. t2.start();
    13. }
    14. }
    15. enum Singleton {
    16. INSTANCE;//枚举项代表实例
    17. }

    优缺点:

    这借助JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建心的对象

    推荐使用


    单例模式注意事项和细节说明

    单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能

    当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new

    单例模式使用的场景,需要频繁的进行创建和销毁的对象,创建对象时耗时过多或耗费资源过多(即重量级对象),但又经常用到的对象,工具类对象,频繁访问数据库或者文件的对象(比如数据源、session工厂等)

  • 相关阅读:
    spring-boot + mybatis-enhance-actable实现自动创建表
    104 使用Ajax请求纯文本
    POI动态字段导出Excel-导入Excel,解析加密数据再导出
    与传统IT开发相比,低代码平台有何优势?
    使用Opencv对图像进行压缩和解压缩
    目标检测YOLO实战应用案例100讲-基于机器视觉的输电线路小目标检测和缺 陷识别
    阿里云服务器配置CPU内存、带宽和系统盘选择方法
    Differential of a function
    前后端分离的Java医院云HIS信息管理系统源码(LIS源码+电子病历源码)
    快速灵敏的 Flink1
  • 原文地址:https://blog.csdn.net/DiligentDog/article/details/127761390