zoukankan      html  css  js  c++  java
  • 单例设计模式

    单例设计模式

    一、单例设计模式介绍

      所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只能提供一个取得其对象实例的方法(静态方法)。

      通俗地说:单例是需要在内存中永远只能创建一个类的实例。

      单例的作用:节约内存和保证共享计算的结果正确,以及方便管理。

      单例的适用场景:

      •  全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
      •  无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。
      •  如 Hibernate 的 SessionFactory,它充当数据存储源的代理,并负责创建 Session 对象。SessionFactory 并不是轻量级的,一般情况下,一个项目通常只需要一个  SessionFactory 就够了。

    二、单例设计模式八种方式

      1、单例设计模式要点

        一、某个类只能有一个实例

          构造器私有化

        二、它必须自行创建这个实例

          含有一个该类的静态变量来保存这个唯一的实例

        三、它必须自行向整个系统提供这个实例

          对外提供获取该实例对象的方法:(1)直接暴露;(2)用静态变量的 get方法获取

      2、单例设计方式

      单例模式我们可以提供出8种写法,有很多时候我们存在饿汉式单例的概念,以及懒汉式单例的概念

      饿汉式单例的含义是:在获取单例对象之前就已经创建完成了。

      饿汉式特点:无论在程序中是否会用到该实例对象,都会提前创建出来,可能造成内存浪费。(饿汉式不涉及线程安全问题,在类初始化时直接创建实例对象)

      懒汉式的单例是指:在真正需要单例的时候才创建出该对象。

      懒汉式特点:在程序中,有时候可能需要推迟一些高开销对象的初始化操作,并且只有在使用这些对象的时候才初始化,此时,就需要采用延迟初始化策略。

      

      单例模式的八种方式:

      1. 饿汉式(静态常量)

      2. 饿汉式(静态代码块)

      3. 懒汉式(线程不安全)

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

      5. 懒汉式(线程安全,同步代码块)

      6. 双重检查

      7. 静态内部类(懒汉式,适用于多线程)

      8. 枚举(饿汉式)

    三、八种方式实现

      1. 饿汉式(静态常量)

        步骤:

    1)构造器私有化(防止 new)

    2)定义一个静态常量保存一个唯一的实例对象(单例)

    3)向外暴露一个静态的公共方法。

        代码实现:

     1 public class Singleton1 {
     2     
     3     //1.构造器私有化,外部不能 new
     4     private Singleton1() {
     5         
     6     }
     7     
     8     //2.本类内部创建对象实例
     9     private static final Singleton1 INSTANCE = new Singleton1();
    10     
    11     //3.提供一个公有的静态方法,返回实例对象
    12     public static Singleton1 getInstance() {
    13         return INSTANCE;
    14     }
    15     
    16 }

        优缺点说明:

        (1)优点:写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

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

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

        (4)总结:这种单例模式可用,可能造成内存浪费。

      2. 饿汉式(静态代码块)

        步骤:

    1)构造器私有化(防止 new)

    2)定义一个静态常量保存一个唯一的实例对象(单例),通过静态代码块初始化单例对象。

    3)向外暴露一个静态的公共方法。

        代码实现:

     1 public class Singleton2 {
     2     
     3     //1.构造器私有化,外部不能  new
     4     private Singleton2() {
     5         
     6     }
     7     
     8     //2.本类内部创建对象实例
     9     private static final Singleton2 INSTANCE;
    10     
    11     //3.在静态代码块中,创建单例对象
    12     static {
    13         INSTANCE = new Singleton2();
    14     }
    15     
    16     //4.提供一个公有的静态方法,返回实例对象
    17     public static Singleton2 getInstance() {
    18         return INSTANCE;
    19     }
    20 }

        优缺点说明:

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

        (2)这种方式可以在构造方法有参数的时候,通过静态代码块给变量进行赋值。

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

        实现2:

     1 public class Singleton {
     2 
     3     private static final Singleton INSTANCE;
     4     
     5     private String info;
     6     static {
     7         try {
     8             Properties pro = new Properties();
     9             //读取外部配置文件
    10             pro.load(Singleton.class.getClassLoader().getResourceAsStream("single.properties"));
    11             //动态获取某个值来给属性赋值
    12             INSTANCE = new Singleton(pro.getProperty("info"));
    13         } catch (IOException e) {
    14             throw new RuntimeException(e);
    15         }
    16     }
    17     
    18     private Singleton(String info) {
    19         this.info = info;
    20     }
    21 }

      

      3. 懒汉式(线程不安全)

        步骤:

    1)构造器私有化

    2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)

    3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象

      如果有就直接返回;如果没有就创建一个新的单例对象。

        代码实现:

     1 public class Singleton3 {
     2     
     3     private static Singleton3 INSTANCE;
     4     
     5     private Singleton3() {
     6         
     7     }
     8     
     9     //提供一个静态的公有方法,当使用到该方法时,才去创建 实例对象
    10     public static Singleton3 getInstance() {
    11         if (INSTANCE == null) {
    12             //说明这是第一次获取单例对象,需要真正的创建出来
    13             INSTANCE = new Singleton3();
    14         }
    15         return INSTANCE;
    16     }
    17 
    18 }

        优缺点说明:

        (1)起到了 Lazy Loading 的效果,但是只能在单线程下使用。

        (2)如果在多线程下,一个线程进入了 if (INSTANCE == null) 判断语句块,还未来的及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

        (3)结论:在实际开发中,不要使用这种方式

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

        步骤:

    1)构造器私有化

    2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)

    3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象

      如果有就直接返回;如果没有就创建一个新的单例对象。

    4)为获取单例的方法加锁: 使用 synchronized 关键字

        代码实现:

     1 public class Singleton4 {
     2     
     3     private static Singleton4 INSTANCE;
     4     
     5     private Singleton4() {
     6         
     7     }
     8     
     9     //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
    10     public static synchronized Singleton4 getInstance() {
    11         if (INSTANCE == null) {
    12             //说明这是第一次获取单例对象,需要真正的创建出来
    13             INSTANCE = new Singleton4();
    14         }
    15         return INSTANCE;
    16     }
    17 }

        优缺点说明:

        (1)解决了线程安全问题

        (2)效率太低了,每个线程在想获得类的时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类的实例,直接 return 就行了。方法进行同步后效率太低。

          使用 synchronized 关键字修改方法包装线程安全,但性能差太多,并发下只能有一个线程正在进入获取单例对象。

        (3)结论:在实际开发中,不推荐使用这种方式

      5. 懒汉式(线程安全,同步代码块)

        步骤:

    1)构造器私有化

    2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)

    3)定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象

      如果有就直接返回;如果没有就创建一个新的单例对象。

    4)为获取单例的方法内部的代码加锁: 使用 synchronized 关键字

        代码实现:

     1 public class Singleton5 {
     2 
     3     private static Singleton5 INSTANCE;
     4     
     5     private Singleton5() {
     6         
     7     }
     8     
     9     //性能得到了优化,但是依然不能保证第一次获取对象的线程安全
    10     public static Singleton5 getInstance() {
    11         //判断单例对象的变量是否为 null
    12         if (INSTANCE == null) {
    13             //很多线程执行到这里来:A,B
    14             //如果是第一次执行,instance为空,A,B 两个线程都会创建对象,并不安全
    15             synchronized (Singleton5.class) {
    16                 INSTANCE = new Singleton5();
    17             }
    18         }
    19         return INSTANCE;
    20     }
    21 
    22 }

        优缺点:

        (1)对比与上面的方式,性能得到了优化

        (2)但本质上还是线程不安全的,多个线程都执行了 if 语句,遇到 synchronized 关键字,会阻塞在这里,所以并不能保证第一次获取对象的线程安全。

        (3)结论:在实际开发中,不推荐使用

      6. 懒汉式(volatile 双重检查模式)

        步骤:

    1)构造器私有化

    2)定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)

    3)提供一个方法进行双重检查机制返回单例对象

    4)必须使用 volatile 修饰静态的变量

        代码实现:

     1 public class Singleton6 {
     2     // 静态属性,volatile保证可见性和禁止指令重排序
     3     private volatile static Singleton6 INSTANCE = null;
     4     
     5     private Singleton6() {
     6         
     7     }
     8     
     9     //提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题,同时保证了效率
    10     public static Singleton6 getInstance() {
    11         //判断单例对象的变量是否为 null
    12         //第一次检查
    13         if (INSTANCE == null) {
    14             //同步锁定代码块
    15             synchronized (Singleton6.class) {
    16                 //第二重检查锁定
    17                 if (INSTANCE == null) {
    18                     //注意:非原子操作
    19                     INSTANCE = new Singleton6();
    20                 }
    21             }
    22         }
    23         
    24         return INSTANCE;
    25     }
    26     
    27 }

        优缺点:

        (1)Double-Check 概念是多线程开发中常使用到的,在代码中,进行了两次 if(INSTANCE == null) 检查,这样就可以保证线程安全了

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

        (3)优点:线程安全,延迟加载,效率较高

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

        分析:为什么要使用 volatile 保证安全?

          1. 禁止指令重排序

              对象实际上创建要进行如下几个步骤:

    a. 分配内存空间;

    b. 调用构造器,初始化实例;

    c. 返回地址给引用;

          所以,new  Singleton() 是一个非原子操作,编译器可能会重排序【构造函数可能在整个对象初始化完成前执行完毕,即赋值操作(只是在内存中开辟一片存储区域后直接返回内存的应用)在初始化对象前完成】。而 线程 C 在线程 A 赋值完时判断 instance 就不为 null,此时 C 拿到的将是一个没有初始化完成的半成品。这样是很危险的。因为极有可能线程C会继续拿着个没有初始化的对象的数据进行操作。此时容易触发“NPE异常NullPointException”。

          图解:

           

         2. 保证可见性

          a. 由于可见性问题,线程 A 在自己的工作线程内创建了实例,但此时还未同步到主存中;此时线程 C 在主存中判断 instance 还是 null,那么线程 C 又将在自己的工作线程中创建一个实例,这样就创建了多个实例。

          b. 如果加上了 volatile 修饰 instance 之后,保证了可见性,一旦线程 A 返回了实例,线程 C 可以立即发现 Instance 不为 null。

      7. 静态内部类

        步骤:

    1)构造器私有化

    2)定义一个内部类,在内部类中定义一个实例对象的常量

    3)提供一个方法进行获取内部类的常量对象。

        代码实现:

     1 public class Singleton7 {
     2     
     3     private Singleton7() {
     4         
     5     }
     6     
     7     //写一个静态内部类,该中有一个静态属性 Singleton
     8     private static class InnerClass {
     9         private static final Singleton7 INSTANCE = new Singleton7();
    10     }
    11     
    12     //提供一个静态的公有方法,直接返回 实例对象
    13     public static Singleton7 getInstance() {
    14         return InnerClass.INSTANCE;
    15     }
    16     
    17 }

        优缺点:

        (1)这种方式采用了类装载的机制来保证初始化实例只有一个线程。

        (2)静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance() 方法,才会装载内部类,从而完成 Singleton 的实例化。

        (3)类的静态属性只会在第一次加载类的时候初始化,所在在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

        (4)优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。

        (5)结论:推荐使用。

         JVM在类初始化阶段(即在 Class 被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以通过静态内部类的方式实现延迟初始化方案。

         小结
        1.  静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上JVM的特性,这种方式又实现了线程安全的创建单例对象。
        2. 通过对比基于 volatile 的双重检查锁定方案和基于类初始化方案的对比,我们会发现基于类初始化的方案的实现代码更简洁。但是基于volatile的双重检查锁定方案有一个额外的优势:除了可以对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化

      8. 枚举

       代码实现:

    1 public enum Singleton8 {
    2     
    3     INSTANCE;  //属性
    4     
    5     private Singleton8() {
    6         
    7     }
    8 
    9 }

       优缺点分析:

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

       (2)结论:推荐使用。

      

    三、单例模式在 JDK 应用的源码分析

      1. JDK中,java.lang.Runtime 就是经典的单例模式(饿汉式)

      2. 代码分析+代码说明

      

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

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

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

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

     

  • 相关阅读:
    Asp.net MVC 中Ajax的使用
    MVC Controller return 格式分类及用法
    【金楽】老博客地址
    C语言博客作业--结构体
    结构体、共用体、枚举博客转载
    C博客作业--指针
    C语言博客作业--字符数组
    C语言博客作业--一二维数组
    C语言博客作业--数据类型
    C语言博客作业--函数
  • 原文地址:https://www.cnblogs.com/niujifei/p/12022757.html
Copyright © 2011-2022 走看看