“你知道茴香豆的‘茴’字有几种写法吗?”

纠结单例模式有几种写法有用吗?有点用,面试中经常选择其中一种或几种写法作为话头,考查设计模式和coding style的同时,还很容易扩展到其他问题。这里讲解几种猴哥常用的写法,但切忌生搬硬套,去记“茴香豆的写法”。编程最大的乐趣在于“know everything, control everything”

JDK版本:oracle java 1.8.0_102

大体可分为4类,下面分别介绍他们的基本形式、变种及特点。

饱汉模式

饱汉是变种最多的单例模式。我们从饱汉出发,通过其变种逐渐了解实现单例模式时需要关注的问题。

基础的饱汉

饱汉,即已经吃饱,不着急再吃,饿的时候再吃。所以他就先不初始化单例,等第一次使用的时候再初始化,即“懒加载”

// 饱汉
// UnThreadSafe
public class Singleton1 {private static Singleton1 singleton = null;private Singleton1() {}public static Singleton1 getInstance() {if (singleton == null) {singleton = new Singleton1();}return singleton;}
}复制代码

饱汉模式的核心就是懒加载。好处是更启动速度快、节省资源,一直到实例被第一次访问,才需要初始化单例;小坏处是写起来麻烦,大坏处是线程不安全,if语句存在竞态条件。

写起来麻烦不是大问题,可读性好啊。因此,单线程环境下,基础饱汉是猴哥最喜欢的写法。但多线程环境下,基础饱汉就彻底不可用了。下面的几种变种都在试图解决基础饱汉线程不安全的问题。

饱汉 - 变种 1

最粗暴的犯法是用synchronized关键字修饰getInstance()方法,这样能达到绝对的线程安全。

// 饱汉
// ThreadSafe
public class Singleton1_1 {private static Singleton1_1 singleton = null;private Singleton1_1() {}public synchronized static Singleton1_1 getInstance() {if (singleton == null) {singleton = new Singleton1_1();}return singleton;}
}复制代码

变种1的好处是写起来简单,且绝对线程安全;坏处是并发性能极差,事实上完全退化到了串行。单例只需要初始化一次,但就算初始化以后,synchronized的锁也无法避开,从而getInstance()完全变成了串行操作。性能不敏感的场景建议使用

饱汉 - 变种 2

变种2是“臭名昭著”的DCL 1.0

针对变种1中单例初始化后锁仍然无法避开的问题,变种2在变种1的外层又套了一层check,加上synchronized内层的check,即所谓“双重检查锁”(Double Check Lock,简称DCL)。

// 饱汉
// UnThreadSafe
public class Singleton1_2 {private static Singleton1_2 singleton = null;private Singleton1_2() {}public static Singleton1_2 getInstance() {// may get half objectif (singleton == null) {synchronized (Singleton1_2.class) {if (singleton == null) {singleton = new Singleton1_2();}}}return singleton;}
}复制代码

变种2的核心是DCL,看起来变种2似乎已经达到了理想的效果:懒加载+线程安全。可惜的是,正如注释中所说,DCL仍然是线程不安全的,由于指令重排序,你可能会得到“半个对象”。详细在看完变种3后,可参考猴子之前的一篇文章,这里不再赘述。

参考:volatile关键字的作用、原理

饱汉 - 变种 3

变种3专门针对变种2,可谓DCL 2.0

针对变种3的“半个对象”问题,变种3在instance上增加了volatile关键字,原理见上述参考。

// 饱汉
// ThreadSafe
public class Singleton1_3 {private static volatile Singleton1_3 singleton = null;private Singleton1_3() {}public static Singleton1_3 getInstance() {if (singleton == null) {synchronized (Singleton1_3.class) {// must be a complete instanceif (singleton == null) {singleton = new Singleton1_3();}}}return singleton;}
}复制代码

多线程环境下,变种3更适用于性能敏感的场景。但后面我们将了解到,就算是线程安全的,还有一些办法能破坏单例。

饿汉模式

与饱汉相对,饿汉很饿,只想着尽早吃到。所以他就在最早的时机,即类加载时初始化单例,以后访问时直接返回即可。

// 饿汉
// ThreadSafe
public class Singleton2 {private static final Singleton2 singleton = new Singleton2();private Singleton2() {}public static Singleton2 getInstance() {return singleton;}
}复制代码

饿汉的好处是天生的线程安全(得益于类加载机制),写起来超级简单,使用时没有延迟;坏处是有可能造成资源浪费(如果类加载后就一直不使用单例的话)。

值得注意的时,单线程环境下,饿汉与饱汉在性能上没什么差别;但多线程环境下,由于饱汉需要加锁,饿汉的性能反而更优。

Holder模式

我们既希望利用饿汉模式中静态变量的方便和线程安全;又希望通过懒加载规避资源浪费。Holder模式满足了这两点要求:核心仍然是静态变量,足够方便和线程安全;通过静态的Holder类持有真正实例,间接实现了懒加载。

// Holder模式
// ThreadSafe
public class Singleton3 {private static class SingletonHolder {private static final Singleton3 singleton = new Singleton3();private SingletonHolder() {}}private Singleton3() {}/*** 勘误:多写了个synchronized。。public synchronized static Singleton3 getInstance() {return SingletonHolder.singleton;}*/public static Singleton3 getInstance() {return SingletonHolder.singleton;}
}复制代码

相对于饿汉模式,Holder模式仅增加了一个静态内部类的成本,与饱汉的变种3效果相当(略优),都是比较受欢迎的实现方式。同样建议考虑

枚举模式

用枚举实现单例模式,相当好用,但可读性是不存在的。

基础的枚举

将枚举的静态成员变量作为单例的实例:

// 枚举
// ThreadSafe
public enum Singleton4 {SINGLETON;
}复制代码

代码量比饿汉模式更少。但用户只能直接访问实例Singleton4.SINGLETON——事实上,这样的访问方式作为单例使用也是恰当的,只是牺牲了静态工厂方法的优点,如无法实现懒加载。

丑陋但好用的语法糖

Java的枚举是一个“丑陋但好用的语法糖”。

枚举型单例模式的本质

通过反编译(jad,源码|String拼接操作”+”的优化?也用到了)打开语法糖,就看到了枚举类型的本质,简化如下:

// 枚举
// ThreadSafe
public class Singleton4 extends Enum<Singleton4> {...public static final Singleton4 SINGLETON = new Singleton4();...
}复制代码

本质上和饿汉模式相同,区别仅在于公有的静态成员变量。

用枚举实现一些trick

这一部分与单例没什么关系,可以跳过。如果选择阅读也请认清这样的事实:虽然枚举相当灵活,但如何恰当的使用枚举有一定难度。一个足够简单的典型例子是TimeUnit类,建议有时间耐心阅读。

上面已经看到,枚举型单例的本质仍然是一个普通的类。实际上,我们可以在枚举型型单例上增加任何普通类可以完成的功能。要点在于枚举实例的初始化,可以理解为实例化了一个匿名内部类。为了更明显,我们在Singleton4_1中定义一个普通的私有成员变量,一个普通的公有成员方法,和一个公有的抽象成员方法,如下:

// 枚举
// ThreadSafe
public enum Singleton4_1 {SINGLETON("enum is the easiest singleton pattern, but not the most readable") {public void testAbsMethod() {print();System.out.println("enum is ugly, but so flexible to make lots of trick");}};private String comment = null;Singleton4_1(String comment) {this.comment = comment;}public void print() {System.out.println("comment=" + comment);}abstract public void testAbsMethod();public static Singleton4_1 getInstance() {return SINGLETON;}
}复制代码

这样,枚举类Singleton4_1中的每一个枚举实例不仅继承了父类Singleton4_1的成员方法print(),还必须实现父类Singleton4_1的抽象成员方法testAbsMethod()

总结

上面的分析都忽略了反射和序列化的问题。通过反射或序列化,我们仍然能够访问到私有构造器,创建新的实例破坏单例模式。此时,只有枚举模式能天然防范这一问题。反射和序列化猴子还不太了解,但基本原理并不难,可以在其他模式上手动实现。

下面继续忽略反射和序列化的问题,做个总结回味一下:

实现方式 关键点 资源浪费 线程安全 多线程环境的性能足够优化
基础饱汉 懒加载 -
饱汉变种1 懒加载、同步
饱汉变种2 懒加载、DCL -
饱汉变种3 懒加载、DCL、volatile
饿汉 静态变量初始化
Holder 静态变量初始化、holder
枚举 枚举本质、静态变量初始化

单例模式是面试中的常考点,写起来非常简单。一方面考查正确性,看本文分析;一方面考查coding style,参考:程序猿应该记住的几条基本规则


本文链接:面试中单例模式有几种写法?
作者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

面试中单例模式有几种写法?相关推荐

  1. 使用严格模式的坏处_再见面试官:单例模式有几种写法?

    点击上方"JAVA",星标公众号重磅干货,第一时间送达 饱汉模式 饿汉模式 Holder模式 枚举模式 丑陋但好用的语法糖 总结 " "你知道茴香豆的'茴'字有 ...

  2. 单例模式的八种写法比较

    转:https://www.cnblogs.com/zhaoyan001/p/6365064.html 单例模式是最常用到的设计模式之一,熟悉设计模式的朋友对单例模式都不会陌生.一般介绍单例模式的书籍 ...

  3. 【设计模式】各个击破单例模式的8种写法

    单例模式 在一个系统开发过程中,我们在基于节省内存资源.保证数据内容的一致性的考虑上,往往需要对某些类要求只能创建一个实例,即「保证类只有一个实例」的设计模式就是单例模式. 比如我们遇到过的各种Man ...

  4. Java设计模式之单例模式(七种写法)

    Java设计模式之单例模式(七种写法) 第一种,懒汉式,lazy初始化,线程不安全,多线程中无法工作: public class Singleton {private static Singleton ...

  5. Java 实现单例模式的七种写法

    单例模式(Singleton Pattern) 是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场. 比如, ...

  6. java中单例的应用_浅谈Java中单例模式的几种应用

    目录 浅谈Java中单例模式的几种应用 第一种:懒汉式 第二种:饿汉式 第三种:双重检索式 第四种:注册登记式 第五种:内部类形式 浅谈Java中单例模式的几种应用 日常开发中,为了提高我们系统中对象 ...

  7. 单例模式的7种写法及其应用场景你都熟悉了吗,快来看看吧

    单例模式的7种写法 定义 单例模式的写法有以下7种 1. 饿汉式(静态常量) 2. 懒汉式(线程不安全) 3. 懒汉式(线程安全,同步方法) 4. 懒汉式(线程安全,同步代码块) 5. 双重检查 6. ...

  8. python实现单例模式的几种方式_基于Python中单例模式的几种实现方式及优化详解...

    单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在.当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场. ...

  9. javascript中面向对象的5种写法

    1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ...

  10. 总结一下Java中SQL的九种写法

    因为在做持久层工具开发,现总结一下各种SQL的写法,也算是清晰一下自已的思路: 第一种写法 public void someMethod(){ try Connection conn=...; con ...

最新文章

  1. 中国制造2025+互联网+,引领制造业发展
  2. php redis 二进制,php-redis扩展
  3. Qt写ini配置文件
  4. 如何在ASP.NET中用OWC绘制图表
  5. best introduction to camera calibration
  6. python路径拼接os.path.join()函数和os.makedirs的用法
  7. DA模拟量控制外接600V高压直流源-设计分析
  8. c语言位运算测试题及答案,[原创] 位运算和bit位操作程序题目答案 -- 谭浩强C语言习题...
  9. 《Storm实时数据处理》一2.7 为日志流集群创建集成测试
  10. java怎么写自定义布局_java-Android设置自定义首选项布局
  11. 牢记31种CSS选择器
  12. PHP set_error_handler() 函数
  13. 流水灯c语言代码大全,单片机闪烁灯流水灯汇编代码大全
  14. GB28181协议常见几种信令流程(一)
  15. 本特利3500软件组态指导
  16. jenkins 下载 安装 启动教程-通过war的方式
  17. 数据库实验报告一-创建数据库和表
  18. YC孵化创业营2020-Adora Cheung - How to Prioritize Your Time 如何分配你时间的优先级
  19. 微信开发工具安装写入失败
  20. python distribute包管理工具安装AttributeError错误

热门文章

  1. java点歌系统代码_基于jsp的KTV点歌系统-JavaEE实现KTV点歌系统 - java项目源码
  2. 想学python网课哪个好过_python入门:想学python网课哪个好?
  3. css3 文字高光划过,CSS3实现一束光划过图片、和文字特效
  4. 《ROS2机器人建模URDF》8.2RVIZ2可视化移动机器人模型
  5. Generative Adversarial Networks overview(1)
  6. 上帝永远不会问你的十件事
  7. 张子阳:如何在30岁前年薪超过30万
  8. 最短路径(dijistra和floyd算法)
  9. nginx反向代理实现直接域名访问
  10. JAVA中黄金分割点的问题