单例设计模式

单例设计模式

一、单例设计模式介绍

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

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

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

  单例的适用场景:

    •  全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
    •  无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。
    •  如 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 工厂等。)

 

原文地址:https://www.cnblogs.com/niujifei/p/12022757.html