参考:1.《图解设计模式》 五种单例模式: https://segmentfault.com/a/1190000018000917 单例模式概念: https://blog.csdn.net/hb_csu/article/details/80504276 JDK中的单例模式: https://blog.csdn.net/wobushixiaobailian/article/details/86744763 静态内部类加载时间: https://www.cnblogs.com/zouxiangzhongyan/p/10762540.html 反射以及反序列化攻击: https://www.cnblogs.com/happy4java/p/11206105.html 枚举怎么保证线程安全: https://blog.csdn.net/ncuzengxiebo/article/details/80754107

模式定义

  • 在软件系统运行过程中,某个类只存在一个实例 :
  • 一个类实现单例模式时需要具备以下3个条件:

    • 类的内部定义一个该类的静态私有成员变量;
    • 构造方法为私有;
    • 提供静态工厂方法,供外部获取类的实例;

      模式优点

      解决何种问题

  • 保证一个类只有一个实例,并且提供一个访问该实例的全局访问入口

    模式好处

  • 由于单例模式只生成一个实例 ,减少了系统给的性能开销,当一个对象需要产生时,当时消耗的资源较多。那么产生对象时构建的方式就可以通过单例去构建 ;

  • 单例模式存在全局访问点,所以可以优化共享资源访问。

    模式实例

  • Windows的任务管理器

  • Windows的回收站,也是一个单例应用
  • 项目中的读取配置文件的对象
  • 数据库的连接池
  • Servlet中的Application Servlet
  • SpringMVC Struts中的控制器 Spring中的Bean默认也是单例的

    模式使用场景

  • 需要频繁实例化然后销毁的对象 ;

  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象 ;
  • 有状态的工具类对象 ;
  • 频繁访问数据库或文件的对象 ;

    JDK里的单例模式

  • java.lang.Runtime

  • java.text. NumberFormat

    1. /**
    2. * Every Java application has a single instance of class
    3. */
    4. public class Runtime {
    5. private static Runtime currentRuntime = new Runtime();
    6. /**
    7. * Returns the runtime object associated with the current Java application.
    8. * Most of the methods of class <code>Runtime</code> are instance
    9. * methods and must be invoked with respect to the current runtime object.
    10. *
    11. * @return the <code>Runtime</code> object associated with the current
    12. * Java application.
    13. */
    14. public static Runtime getRuntime() {
    15. return currentRuntime;
    16. }
    17. /** Don't let anyone else instantiate this class */
    18. private Runtime() {}
    19. ....
    20. }

    各种单例代码

    1. /**
    2. * 饿汉式:线程安全,调用效率高,但是不能延时加载
    3. * 缺点:系统加载时消耗额外资源,如果该实例没有使用的情况会造成资源浪费
    4. *
    5. */
    6. public class Singleton1 {
    7. //在类加载式自行实例化对象,线程安全的,因为类只加载一次
    8. //类初始化时,立即加载这个对象
    9. private static Singleton1 instance = new Singleton1();
    10. //构造器私有 无法创建对象
    11. private Singleton1() {
    12. }
    13. //对外提供get方法获取 该类的实例
    14. public static Singleton1 getInstance() {
    15. return instance;
    16. }
    17. }

    饿汉模式在类被初始化时就已经在内存中创建了对象,以空间换时间,故不存在线程安全问题。

类的初始化和实例化问题:https://blog.csdn.net/justloveyou_/article/details/72466416

  1. /**
  2. * 懒汉式,线程不安全
  3. */
  4. public class Singleton2 {
  5. private static Singleton2 instance = null ;
  6. private Singleton2 () {
  7. }
  8. //调用时加载对象
  9. //下面这段代码 不是原子性操作 会出现线程安全问题
  10. //如果想要线程安全,可加上synchronized关键字,但是这样效率低下
  11. public /**synchronized*/static Singleton2 getInstance() {
  12. if (instance == null) {
  13. instance = new Singleton2();
  14. }
  15. return instance;
  16. }
  17. }

懒汉模式在方法被调用后才创建对象,以时间换空间,在多线程环境下存在风险。

  1. /**
  2. * 懒汉式(双重同步锁)
  3. */
  4. public class Singleton3 {
  5. public static volatile Singleton3 instance = null ;
  6. private Singleton3() {
  7. }
  8. /**
  9. * 解决了性能问题,来自JVM的指令重排序
  10. * 解决:在对象中添加volatile 关键字来 不让jvm对该 对象做优化
  11. * @return
  12. */
  13. public static Singleton3 getInstance() {
  14. if (instance == null) {
  15. synchronized (Singleton3.class) {
  16. if (instance == null) {
  17. instance = new Singleton3();
  18. }
  19. }
  20. }
  21. return instance;
  22. }
  23. }

DCL模式的优点就是,只有在对象需要被使用时才创建,第一次判断 INSTANCE == null为了避免非必要加锁,当第一次加载时才对实例进行加锁再实例化。这样既可以节约内存空间,又可以保证线程安全。但是,由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况。具体分析如下:
INSTANCE = new SingleTon();
这个步骤,其实在jvm里面的执行分为三步:

1.在堆内存开辟内存空间。
2.在堆内存中实例化SingleTon里面的各个参数。
3.把对象指向堆内存空间。

由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。

不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,即在JDK1.6及以后,只要定义为private volatile static SingleTon INSTANCE = null;就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

  1. /**
  2. * 静态内部类
  3. * 静态内部类式和饿汉式一样,同样利用了ClassLoader的机制保证了线程安全
  4. * 同的是,饿汉式在Singleton类被加载时就创建了一个实例对象,
  5. * 而静态内部类即使Singleton类被加载也不会创建单例对象,
  6. * 除非调用里面的getInstance()方法。因为当Singleton类被加载时
  7. * 其静态内部类SingletonHolder没有被主动使用。只有当调用getInstance方法时,
  8. * 才会装载SingletonHolder类,从而实例化单例对象。
  9. * 这样,通过静态内部类的方法就实现了lazy loading,很好地将懒汉式和饿汉式结合起来,
  10. * 既实现延迟加载,保证系统性能,也能保证线程安全
  11. */
  12. public class Singleton4 {
  13. private static class SingleInnerHolder {
  14. private static Singleton4 instance = new Singleton4();
  15. }
  16. private Singleton4() {
  17. }
  18. public static Singleton4 getInstance() {
  19. return SingleInnerHolder.instance;
  20. }
  21. }

来源:深入理解单例模式:静态内部类单例原理

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。

类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
1.遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
3.当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是”有且仅有”,那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

我们再回头看下getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。**如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。**

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

  1. /**
  2. * @Author:ling
  3. * @Date:2020/4/23 13:19
  4. * @Version:1.0
  5. * 枚举单例
  6. */
  7. public enum Singleton5 {
  8. //实例化对象
  9. INSTANCE ;
  10. void getInstance() {
  11. }
  12. }

关于枚举的线程安全,反射还有序列化问题,详情请看笔记:https://www.yuque.com/woyubugtongzai/gb7au2/ryse31


模式总结

  • 懒汉式效率最低;
  • 占用资源少 不需要延时加载 枚举优于 饿汉式 ;
  • 占用资源比较多 需要延时加载 静态内部类 优于 懒汉式 。