什么是单例模式

单例模式(Singleton Pattern),顾名思义,就是被单例的对象只能有一个实例存在。单例模式的实现方式是,一个类能返回对象的一个引用(永远是同一个)和一个获得该唯一实例的方法(必须是静态方法)。通过单例模式,我们可以保证系统中只有一个实例,从而在某些特定的场合下达到节约或者控制系统资源的目的。

单例模式类图

在 【装饰者模式】中,我们体验了拥有各种不同特性的女朋友的 “酸爽”... 不过梦想很丰满,现实很骨感,最后你只能拥有一个老婆。

单例模式示例代码

在学习本课之前,你也许已经听说或者了解过单例模式了。但你知道单例模式有多少种方式实现吗?这个看起来最为简单的设计模式,其实有很多坑...

1.饿汉模式

最常见、最简单的单例模式写法之一。顾名思义,“饿汉模式” 就是很 “饥渴”,所以一上来就需要给它新建一个实例。但这种方法有一个明显的缺点,那就是不管有没有调用过获得实例的方法(本例中为 getWife() ),每次都会新建一个实例。

// 饿汉模式
public class Wife {// 一开始就新建一个实例private static final Wife wife = new Wife();// 默认构造方法private Wife() {}// 获得实例的方法public static Wife getWife() {return wife;}
}

2.懒汉模式

最常见、最简单的单例模式之二,跟 “饿汉模式” 是 “好基友”。再次顾名思义,“懒汉模式” 就是它很懒,一开始不新建实例,只有当它需要使用的时候,会先判断实例是否为空,如果为空才会新建一个实例来使用。

// 懒汉模式
public class Wife {//一开始没有新建实例private static Wife wife;private Wife() { }// 需要时再新建public static Wife getWife() {if (wife == null) {wife = new Wife();}return wife;}
}

3.线程安全的懒汉模式

是不是感觉很简单?但是上面的懒汉模式却存在一个严重的问题。那就是如果有多个线程并行调用 getWife() 方法的时候,还是会创建多个实例,单例模式就失效了。

Bug 来了,改改改!

简单,我们在基本的懒汉模式上,把它设为线程同步(synchronized)就好了。synchronized 的作用就是保证在同一时刻最多只有一个线程运行,这样就避免了多线程带来的问题。关于 synchronized 关键字,你可以 点击这里 了解更多。

// 懒汉模式(线程安全)
public class Wife {private static Wife wife;private Wife() { }// 添加了 synchronized 关键字public static synchronized Wife getWife() {if (wife == null) {wife = new Wife();}return wife;}
}

4.双重检验锁(double check)

线程安全的懒汉模式解决了多线程的问题,看起来完美了。但是它的效率不高,每次调用获得实例的方法 getWife() 时都要进行同步,但是多数情况下并不需要同步操作(例如我的 wife 实例并不为空可以直接使用的时候,就不需要给 getWife() 加同步方法,直接返回 wife 实例就可以了)。所以只需要在第一次新建实例对象的时候,使用同步方法。

不怕,程序猿总是有办法的。于是,在前面的基础上,又有了 “双重检验锁” 的方法。

// 双重锁的 getWife() 方法
public static Wife getWife() {// 第一个检验锁,如果不为空直接返回实例对象,为空才进入下一步if (wife == null) {synchronized (Wife.class) {//第二个检验锁,因为可能有多个线程进入到 if 语句内if (wife == null) {wife = new Wife();}}}return wife ;
}

你以为这终于圆满了?NO...Too young, too naive! 主要问题在于 wife = new Wife() 这句代码,因为在 JVM(Java 虚拟机)执行这句代码的时候,要做好几件事情,而 JVM 为了优化代码,有可能造成做这几件事情的执行顺序是不固定的,从而造成错误。(为了不把问题更加复杂化,这里没有深入讲解在 JVM 中具体是怎么回事,有兴趣的同学可以点击 这里 自行了解下。)

下面是文章中的引用:

另外,再引用IcyFenix文章里面的一段话,会解释清楚双锁检测的局限性:

我们来看看这个场景:假设线程一执行到instance = new SingletonExample()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情: 

1.给SingletonExample的实例分配内存。

2.初始化SingletonExample的构造器

3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。

 

但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误估计调试上一星期都未必能找得出来,真是一茶几的杯具啊。

 

DCL的写法来实现单例是很多技术书、教科书(包括基于JDK1.4以前版本的书籍)上推荐的写法,实际上是不完全正确的。的确在一些语言(譬如C语言)上DCL是可行的,取决于是否能保证2、3步的顺序。在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字,因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonExample instance = null;”就可以保证每次都去instance都从主内存读取,就可以使用DCL的写法来完成单例模式。当然volatile或多或少也会影响到性能,最重要的是我们还要考虑JDK1.42以及之前的版本,所以本文中单例模式写法的改进还在继续。

这个时候,我们需要给实例加一个 volatile 关键字,它的作用就是防止编译器自行优化代码。最后,我们的 “双重检验锁” 版本终于出炉了...

// 双重检验锁
public class Wife {private volatile static Wife wife;private Wife() { }public static Wife getWife() {if (wife == null) {synchronized(Wife.class) {if (wife == null) {wife = new Wife();}}}return wife;}
}

5.静态内部类

上面的方法,修修补补,实在是太复杂了... 而且 volatile 关键字在某些老版本的 JDK 中无法正常工作。咱们得换一种方法,即 “静态内部类”。这种方式,利用了 JVM 自身的机制来保证线程安全,因为 WifeHolder 类是私有的,除了 getWife() 之外没有其它方式可以访问实例对象,而且只有在调用 getWife() 时才会去真正创建实例对象。(这里类似于 “懒汉模式”)

// 静态内部类
public class Wife {private static class WifeHolder {private static final Wife wife = new Wife();}private Wife() { }public static Wife getWife() {return WifeHolder.wife;}
}

6.枚举

还不懂什么是枚举的,先 点这里 补补课。

如下,代码简直是简单得不能再简单了。我们可以通过 Wife.INSTANCE 来访问实例对象,这比 getWife() 要简单得多,而且创建枚举默认就是线程安全的,还可以防止反序列化带来的问题。这么 优(niu)雅(bi)的方法,来自于新版 《Effective Java》 这本书。这种方式虽然不常用,但是最为推荐。

// 枚举
public enum Wife {INSTANCE;// 自定义的其他任意方法public void whateverMethod() { }
}

单例模式的应用

当你只需要一个实例对象的时候,就可以考虑使用单例模式。比如在资源共享的情况下,避免由于多个资源操作导致的性能或损耗等就可以使用单例模式。

这个是面试的时候经常会考的点,面试题通常的问法是:

什么是单例模式?

回答的时候,要答到三元素
1. 构造方法私有化
2. 静态属性指向实例
3. public static的 getInstance方法,返回第二步的静态属性

Java学习笔记之设计模式(7)单例模式相关推荐

  1. java学习笔记13--反射机制与动态代理

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note13.html,转载请注明源地址. Java的反射机制 在Java运行时环境中,对于任意 ...

  2. java学习笔记十三

    11. 凡是继承了FilterOutputStream或FilterInputStream的类都是过滤流,也就是说他们不能直接跟目标(键盘,文件,网络等,节点流可以)数据打交道,只能包装 Intput ...

  3. 《Java学习笔记(第8版)》学习指导

    <Java学习笔记(第8版)>学习指导 目录 图书简况 学习指导 第一章 Java平台概论 第二章 从JDK到IDE 第三章 基础语法 第四章 认识对象 第五章 对象封装 第六章 继承与多 ...

  4. Java学习笔记(原创)

    Java学习笔记(原创) 2011-12-01 16:37:00|  分类: Java|举报|字号 订阅 下载LOFTER客户端 基本知识 一. Java基础 1. java语言的特点: ①简单:没有 ...

  5. java学习笔记11--Annotation

    java学习笔记11--Annotation Annotation:在JDK1.5之后增加的一个新特性,这种特性被称为元数据特性,在JDK1.5之后称为注释,即:使用注释的方式加入一些程序的信息. j ...

  6. 准备写java学习笔记

    准备写java学习笔记 java int 转载于:https://blog.51cto.com/cryingcloud/1975267

  7. Java学习笔记--StringTokenizer的使用

    2019独角兽企业重金招聘Python工程师标准>>> Java Tips: 使用Pattern.split替代String.split String.split方法很常用,用于切割 ...

  8. java学习笔记12--异常处理

    java学习笔记系列: java学习笔记11--集合总结 java学习笔记10--泛型总结 java学习笔记9--内部类总结 java学习笔记8--接口总结 java学习笔记7--抽象类与抽象方法 j ...

  9. Java学习笔记(十)--控制台输入输出

    输入输出 一.控制台输入 在程序运行中要获取用户的输入数据来控制程序,我们要使用到 java.util 包中的 Scanner 类.当然 Java 中还可以使用其他的输入方式,但这里主要讲解 Scan ...

  10. java学习笔记16--I/O流和文件

    本文地址:http://www.cnblogs.com/archimedes/p/java-study-note16.html,转载请注明源地址. IO(Input  Output)流 IO流用来处理 ...

最新文章

  1. redis序列化_实例讲解Springboot以Template方式整合Redis及序列化问题
  2. 宜昌远安谋定功能性-农业大健康·万祥军:绿色和谐新路
  3. Java 最高均薪 19015 元! 8 月程序员工资出炉,你拖后腿了吗?
  4. SCI、EI、ISTP、ISR、SSCI、AHCI简介
  5. Oracle JOB异常中断原因分析
  6. mount -o nolock
  7. java api存在问题改进措施_Sonar常见问题解决方案
  8. Neo4j ---windows下载安装neo4j
  9. ORA-00932: 数据类型不一致: 应为 -, 但却获得 BLOB
  10. 自己合成制作支付宝、微信、QQ三合一万能收款码
  11. 阴阳师服务器维护3月25日,《阴阳师》手游3月25日维护更新公告
  12. Laravel开发的一元交友盲盒源码存取小纸条盲盒交友匹配交友趣味交友同城交友流量
  13. 【高数-2】多元函数最值
  14. linux安装gcc运行时库,现在可以在CentOS 8系统下用命令安装GCC 8.3.1版本
  15. 测度论与概率论笔记4:测度空间上的积分(上)
  16. 走进VR开发世界(4)——走进VR游戏开发的世界
  17. 德国推出新冠疫情监测APP,鼓励民众上传智能手环与手表的健康数据
  18. 关于springboot项目连接oracle数据库报错 ORA01017的改正
  19. 对麦克斯韦方程的理解--1
  20. linux测试带宽ipf3,Linux测试网速之iperf3实用案例

热门文章

  1. JavaScript学习笔记——DOM基础 2.1
  2. session的保存方式.
  3. 服务器虚拟盘怎么设置,服务器虚拟内存设置在什么盘
  4. java ftl 模板 输出list_关于在freemarker模板中遍历数据模型ListJavaBean的经验
  5. SpringApplication#run⽅法第5步,打印banner(四)
  6. Oracle常见索引扫描方式总结
  7. 树和森林(Tree and Forest)
  8. MySQL中的时间函数
  9. MyBatis内的Mapper接口方法为什么不能重载
  10. Ajax的工具类AjaxUtils,使用struts返回Json类型