关于单例模式,我的博客中有很多文章介绍过。作为23种设计模式中最为常用的设计模式,单例模式并没有想象的那么简单。因为在设计单例的时候要考虑很多问题,比如线程安全问题、序列化对单例的破坏等。

单例相关文章一览:

设计模式(二)——单例模式

设计模式(三)——JDK中的那些单例

单例模式的七种写法

单例与序列化的那些事儿

不使用synchronized和lock,如何实现一个线程安全的单例?

不使用synchronized和lock,如何实现一个线程安全的单例?(二)

如果你对单例不是很了解,或者对于单例的线程安全问题以及序列化会破坏单例等问题不是很清楚,可以先阅读以上文章。上面六篇文章看完之后,相信你一定会对单例模式有更多,更深入的理解。

我们知道,单例模式,一般有七种写法,那么这七种写法中,最好的是哪一种呢?为什么呢?本文就来抽丝剥茧一下。

哪种写单例的方式最好

在StakcOverflow中,有一个关于What is an efficient way to implement a singleton pattern in Java?的讨论:

如上图,得票率最高的回答是:使用枚举。

回答者引用了Joshua Bloch大神在《Effective Java》中明确表达过的观点:

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

如果你真的深入理解了单例的用法以及一些可能存在的坑的话,那么你也许也能得到相同的结论,那就是:使用枚举实现单例是一种很好的方法。

枚举单例写法简单

如果你看过《单例模式的七种写法》中的实现单例的所有方式的代码,那就会发现,各种方式实现单例的代码都比较复杂。主要原因是在考虑线程安全问题。

我们简单对比下“双重校验锁”方式和枚举方式实现单例的代码。

“双重校验锁”实现单例:

public class Singleton {  private volatile static Singleton singleton;  private Singleton (){}  public static Singleton getSingleton() {  if (singleton == null) {  synchronized (Singleton.class) {  if (singleton == null) {  singleton = new Singleton();  }  }  }  return singleton;  }
}

枚举实现单例:

public enum Singleton {  INSTANCE;  public void whateverMethod() {  }
}

相比之下,你就会发现,枚举实现单例的代码会精简很多。

上面的双重锁校验的代码之所以很臃肿,是因为大部分代码都是在保证线程安全。为了在保证线程安全和锁粒度之间做权衡,代码难免会写的复杂些。但是,这段代码还是有问题的,因为他无法解决反序列化会破坏单例的问题。

枚举可解决线程安全问题

上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?

其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。

那么,“底层”到底指的是什么?

这就要说到关于枚举的实现了。这部分内容可以参考我的另外一篇博文深度分析Java的枚举类型—-枚举的线程安全性及序列化问题,这里我简单说明一下:

定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。

通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义。

而且,枚举中的各个枚举项同事通过static来定义的。如:

public enum T {SPRING,SUMMER,AUTUMN,WINTER;
}

反编译后代码为:

public final class T extends Enum
{//省略部分内容public static final T SPRING;public static final T SUMMER;public static final T AUTUMN;public static final T WINTER;private static final T ENUM$VALUES[];static{SPRING = new T("SPRING", 0);SUMMER = new T("SUMMER", 1);AUTUMN = new T("AUTUMN", 2);WINTER = new T("WINTER", 3);ENUM$VALUES = (new T[] {SPRING, SUMMER, AUTUMN, WINTER});}
}

了解JVM的类加载机制的朋友应该对这部分比较清楚。static类型的属性会在类被加载之后被初始化,我们在深度分析Java的ClassLoader机制(源码级别)和Java类的加载、链接和初始化两个文章中分别介绍过,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。

也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。

所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。

枚举可解决反序列化会破坏单例的问题

前面我们提到过,就是使用双重校验锁实现的单例其实是存在一定问题的,就是这种单例有可能被序列化锁破坏,关于这种破坏及解决办法,参看单例与序列化的那些事儿,这里不做更加详细的说明了。

那么,对于序列化这件事情,为什么枚举又有先天的优势了呢?答案可以在Java Object Serialization Specification 中找到答案。其中专门对枚举的序列化做了如下规定:

大概意思就是:在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.EnumvalueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve等方法。

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。这部分内容在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中也有更加详细的介绍,还展示了部分代码,感兴趣的朋友可以前往阅读。

总结

在所有的单例实现方式中,枚举是一种在代码写法上最简单的方式,之所以代码十分简洁,是因为Java给我们提供了enum关键字,我们便可以很方便的声明一个枚举类型,而不需要关心其初始化过程中的线程安全问题,因为枚举类在被虚拟机加载的时候会保证线程安全的被初始化。

除此之外,在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。

为什么我墙裂建议大家使用枚举来实现单例。相关推荐

  1. 为什么我强烈建议大家使用枚举来实现单例

    转载自   为什么我墙裂建议大家使用枚举来实现单例 关于单例模式,我的博客中有很多文章介绍过.作为23种设计模式中最为常用的设计模式,单例模式并没有想象的那么简单.因为在设计单例的时候要考虑很多问题, ...

  2. 用枚举enum实现单例

    [README] 1,effectivejava 讲到使用 枚举类实现单例的例子,非常好用: 2,好处如下: 不用定义私有构造器: 不用定义获取单例的方法,如 getInstance() :通过 枚举 ...

  3. java枚举来实现单例_枚举实现单例模式

    Java中的枚举和其它语言不同,它是一个对象.早期的 Java 是没有枚举类型的大家都用类似于单例的方式来实现枚举,简单的说就是让构造 private 化,在 static 块中产生多个 final ...

  4. 为什么枚举实现的单例比较受推崇?有哪些好处?

    1.枚举单例写法简单 public enum Singleton { INSTANCE; public void whateverMethod() { } } 2.枚举可解决线程安全问题 (1)枚举中 ...

  5. 10个非常适合菜鸟练手的Python项目,墙裂建议收藏!

    点上方"菜鸟学Python",选择"星标" 第458篇原创干货,第一时间送达 对于Python学习者来说,能够熟练的掌握Python中简洁而高效的编程技巧,不仅 ...

  6. 【啃书C++Primer5】-c++有些理论基础需要了解,墙裂建议看看原书,有太多细节需要注意了

    任何常用的编程语言都具备一组公共的语法特征,不同语言仅在特征的细节上有所区别.要想学习并掌握–种编程语言,理解其语法特征的实现细节是第一步.最基本的特征包括: 整型.字符型等内置类型变量,用来为对象命 ...

  7. 墙裂建议收藏,100道Python练手题目

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 转自丨Python知识大全 ‍来源丨大学 github.com/R ...

  8. 万字长文,一文读懂Linux的常规操作(墙裂建议收藏)

    前言 以下是我为大家准备的几个精品专栏,喜欢的小伙伴可自行订阅,你的支持就是我不断更新的动力哟! MATLAB-30天带你从入门到精通 MATLAB深入理解高级教程(附源码) tableau可视化数据 ...

  9. 大神崛起必备的10大练手的Python项目 墙裂建议收藏!

    Python毫无疑问是一门很伟大的语言,最近几年,很多人都在Python的世界里转悠.也有很多对编程有着浓厚的兴趣但是不是相关专业的朋友开始学习Python,这固然是好事情.但是很多时候,你只是看到了 ...

最新文章

  1. python编程有哪些-Python编程开发都有哪些限制
  2. 文巾解题 180. 连续出现的数字
  3. Java设计模式——为什么要用枚举实现单例模式(避免反射、序列化问题)
  4. Perhaps you are running on a JRE rather than a JDK?
  5. oracle存储过程多条件判断,oracle存储过程条件替空的判断(2)
  6. superobject内存泄漏
  7. 警卫站岗(树上dp)
  8. APMServ5.2.6 升级php5.2 到 5.3版本及Memcache升级!
  9. python云计算架构开发_云计算开发一般负责什么工作呢?更偏向于运维么?
  10. hibench测试出现问题--zookeeper
  11. 20200504:力扣187周赛下
  12. Visual Studio 开发环境安装与配置
  13. 马哥linux视频笔记,马哥linux培训第四天笔记
  14. matlab画累计直方图_科学网—matlab 绘制直方图——常用命令 - 范凯波的博文
  15. linux WIFI命令iwlist、iwconfig、iwpriv
  16. 1005打印任务取消不了 hp_1005打印任务取消不了 hp_解决hp1005打印机无法打印故障...
  17. css html5布局方式_创建新HTML5 / CSS3单页布局–艺术主题
  18. Typro使用以及安装教程来啦
  19. 百倍加速:Python量化策略的算法性能提升指南
  20. arr.filter()

热门文章

  1. android图片闪动动画,android图片闪烁或帧动画
  2. php 观察者模式怎么用,php怎么实现观察者模式
  3. java calendar 转换_[java]转:String Date Calendar之间的转换
  4. 突破5G壁垒,睿至科技集团以技术实力赋能产业发展
  5. Android Studio 安装 PlantUML 插件
  6. 操作系统之进程管理:18、预防死锁
  7. 操作系统之进程管理:5、处理机调度
  8. 浅析libcurl多线程安全问题
  9. Qt下QTableWidget的使用
  10. CORS漏洞的利用方式(精)