设计模式之 - 单例模式

本贴最后更新于 1672 天前,其中的信息可能已经时移世易

设计模式详解。

《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为"四人组(Gang of Four)"。

设计模式不是一种规定,也不需要为了设计模式而设计,错误的应用设计模式反而会让代码变得复杂,难以维护。相反,正确的应用设计模式,可以让我们的代码更加健壮,扩展性更好,逻辑也更加清晰。

设计模式一书针对不同的应用场景,列出了各自的最佳实践。随着技术的不断发展,可能一些范例已经不再适用,但是这本书带给我们思想上的指引,意义是非常重大的。设计模式一书,根据应用场景,大致分为建造型、结构型、行为型,总共 23 种,并提出了 6 大原则。

本文开始,试着详细介绍每种设计模式,有解释不到位,理解存在偏差的地方,请提出宝贵意见。

建造型-单例模式

单例模式 Singleton,即一个实例,类本身提供获取全局唯一的实例的方式,其它人使用的时候不需要重新创建对象,只需要通过类获取实例即可。

应用场景:配置类、工厂类等

关键点:

1、将实例变成成员变量。 2、将默认构造方法改为private。

优点:

1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。 2、避免对资源的多重占用(比如写文件操作)。

实现方式:分为懒汉式 Lazy ,饿汉式,下面依次介绍


01

饿汉式

**
**

所谓饿汉式,即程序从硬盘加载到内存中,即实现初始化,由 JVM 实现,线程安全。

**
**

方式 1

public class Singleton01 { private static final Singleton01 INSTANCE=new Singleton01(); private Singleton01(){} public void m(){ System.out.println("hello singleton1"); } public static Singleton01 getInstance(){ return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton01.INSTANCE.hashCode())).start(); } }}

输出:

1059951119
1059951119
1059951119
1059951119
1059951119
1059951119
1059951119
1059951119
1059951119

可以看到打印的是同一个对象。

方式 2 将实例过程放到静态代码块中

public class Singleton02 { private static final Singleton02 INSTANCE; static { INSTANCE = new Singleton02(); } private Singleton02() { } public void m() {        System.out.println("hello singleton2"); } public static Singleton02 getInstance() { return INSTANCE; } public static void main(String[] args) {        for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton02.INSTANCE.hashCode())).start(); } }}

输出:

2062686098206268609820626860982062686098206268609820626860982062686098206268609820626860982062686098

方式 3 使用枚举的方式

public enum Singleton08 { INSTANCE ; public void m(){ System.out.println("hello singleton8"); } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton08.INSTANCE.hashCode())).start(); } }}

输出:

2062686098206268609820626860982062686098206268609820626860982062686098206268609820626860982062686098

方式 1 和 2,除了创建实例的过程,其余均相同,理论上没有区别,比较简单,应用也最多。方式 3 使用枚举实现,这种方式是非常安全的,不能通过反射创建实例。整体来看,饿汉式这种方式,也存在一些问题,即我们没有使用这个对象,但是已经实例化了,能不能在我使用的时候才进行实例化呢,那就是懒汉式,即懒加载。下面演示一下懒汉式实现方法。

02

懒汉式

**
**

懒汉式,即在第一次调用的时候才真正的实例化,在加载的时候不进行实例化。

方式 4

public class Singleton03 { private static Singleton03 instance; private Singleton03() { } public void m() { System.out.println("hello singleton3"); }    public static Singleton03 getInstance() { if (null == instance) { instance = new Singleton03(); } return instance; }    public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton03.getInstance().hashCode())).start(); } }}

输出:

2062686098105995111910599511191059951119105995111910599511191059951119105995111910599511191059951119

可以看到,多线程的情况下,会概率出现创建多个对象的情况,那是因为,当一个线程发现当前实例为 null,需要创建一个对象,在这个过程中另一个线程也发现实例为 null,也去创建实例,导致出现多个实例,因此我们使用方式 4 来解决。

方式 5

public class Singleton04 { private static Singleton04 instance; private Singleton04() { } public void m() { System.out.println("hello singleton4"); } public static synchronized Singleton04 getInstance() { if (null == instance) { try { instance = new Singleton04(); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton04.getInstance().hashCode())).start(); } }}

输出:

1585461681158546168115854616811585461681158546168115854616811585461681158546168115854616811585461681

此方式在方法上面增加 synchronized 关键字,将方法变成同步方法,因为 synchronized 关键字使用的重量级锁,上下文切换影响效率,为了减少锁的范围,可能会想到下面的方式优化代码。

方式 6

public class Singleton05 { private static Singleton05 instance; private Singleton05() { } public static Singleton05 getInstance() { if (null == instance) { synchronized (Singleton05.class) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } instance = new Singleton05(); } } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> System.out.println(Singleton05.getInstance().hashCode())).start(); } }}

输出:

29574062727790602098586510057349981047393000410312950991585461681206268609815881478521059951119

运行发现,这样是无法实现,因为多线程运行时,速度非常快,导致多个线程同时阻塞住,而获取到锁的时候就会去创建实例,因此会产生多个实例,因此需要使用下面的方式。

方式 7:DCL 方式(Double Check Locking)

public class Singleton06 { private static volatile Singleton06 instance; private Singleton06() { } public static Singleton06 getInstance() { if (null == instance) { synchronized (Singleton06.class) { if (null == instance) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } instance = new Singleton06(); } } } return instance; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> System.out.println(Singleton06.getInstance().hashCode())).start(); } }}

输出:

2062686098206268609820626860982062686098206268609820626860982062686098206268609820626860982062686098

此种方式是比较完美的解决方式。

双重检查的意义:线程由于无法获取锁,处于阻塞状态,当线程获取锁时,第二次检查一下是否对象创建完成,如果已创建完成,直接返回此对象即可。

DCL 加 volatile 的意义:保证线程可见,防止指令重排序

方式 8:内部类的方式

public class Singleton07 { private Singleton07() { } private static class SingletonHold{ private final static Singleton07 INSTANCE=new Singleton07(); } public static Singleton07 getInstance() { return SingletonHold.INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()-> System.out.println(Singleton07.getInstance().hashCode())).start(); } }}

输出:

868202916868202916868202916868202916868202916868202916868202916868202916868202916868202916

使用内部类,可以将在类加载的时候不会实例化,但是调用的时候会进行实例化,比较好的方式,也是线程安全的。

总结:

单例模式是非常常用的设计模式,看似简单,其实这里面还是有许多值得研究学习。

设计模式不是为了生搬硬套,应该根据实际情况,灵活的运用,后面会介绍更多的设计模式和设计原则,欢迎大家一起讨论学习。

欢迎关注我们

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3202 引用 • 8217 回帖 • 1 关注
  • 设计模式

    设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    201 引用 • 120 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
zylei21
努力做喜欢的事,做到极致 北京

推荐标签 标签

  • Wide

    Wide 是一款基于 Web 的 Go 语言 IDE。通过浏览器就可以进行 Go 开发,并有代码自动完成、查看表达式、编译反馈、Lint、实时结果输出等功能。

    欢迎访问我们运维的实例: https://wide.b3log.org

    30 引用 • 218 回帖 • 643 关注
  • 外包

    有空闲时间是接外包好呢还是学习好呢?

    26 引用 • 233 回帖
  • LaTeX

    LaTeX(音译“拉泰赫”)是一种基于 ΤΕΧ 的排版系统,由美国计算机学家莱斯利·兰伯特(Leslie Lamport)在 20 世纪 80 年代初期开发,利用这种格式,即使使用者没有排版和程序设计的知识也可以充分发挥由 TeX 所提供的强大功能,能在几天,甚至几小时内生成很多具有书籍质量的印刷品。对于生成复杂表格和数学公式,这一点表现得尤为突出。因此它非常适用于生成高印刷质量的科技和数学类文档。

    12 引用 • 59 回帖
  • Mac

    Mac 是苹果公司自 1984 年起以“Macintosh”开始开发的个人消费型计算机,如:iMac、Mac mini、Macbook Air、Macbook Pro、Macbook、Mac Pro 等计算机。

    167 引用 • 597 回帖 • 1 关注
  • 阿里巴巴

    阿里巴巴网络技术有限公司(简称:阿里巴巴集团)是以曾担任英语教师的马云为首的 18 人,于 1999 年在中国杭州创立,他们相信互联网能够创造公平的竞争环境,让小企业通过创新与科技扩展业务,并在参与国内或全球市场竞争时处于更有利的位置。

    43 引用 • 221 回帖 • 54 关注
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 407 关注
  • Lute

    Lute 是一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。

    29 引用 • 202 回帖 • 30 关注
  • JetBrains

    JetBrains 是一家捷克的软件开发公司,该公司位于捷克的布拉格,并在俄国的圣彼得堡及美国麻州波士顿都设有办公室,该公司最为人所熟知的产品是 Java 编程语言开发撰写时所用的集成开发环境:IntelliJ IDEA

    18 引用 • 54 回帖
  • Sphinx

    Sphinx 是一个基于 SQL 的全文检索引擎,可以结合 MySQL、PostgreSQL 做全文搜索,它可以提供比数据库本身更专业的搜索功能,使得应用程序更容易实现专业化的全文检索。

    1 引用 • 225 关注
  • Maven

    Maven 是基于项目对象模型(POM)、通过一小段描述信息来管理项目的构建、报告和文档的软件项目管理工具。

    188 引用 • 319 回帖 • 237 关注
  • 爬虫

    网络爬虫(Spider、Crawler),是一种按照一定的规则,自动地抓取万维网信息的程序。

    106 引用 • 275 回帖 • 1 关注
  • API

    应用程序编程接口(Application Programming Interface)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

    79 引用 • 431 回帖 • 1 关注
  • OkHttp

    OkHttp 是一款 HTTP & HTTP/2 客户端库,专为 Android 和 Java 应用打造。

    16 引用 • 6 回帖 • 88 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 1 关注
  • 职场

    找到自己的位置,萌新烦恼少。

    127 引用 • 1708 回帖 • 2 关注
  • Solidity

    Solidity 是一种智能合约高级语言,运行在 [以太坊] 虚拟机(EVM)之上。它的语法接近于 JavaScript,是一种面向对象的语言。

    3 引用 • 18 回帖 • 446 关注
  • 微软

    微软是一家美国跨国科技公司,也是世界 PC 软件开发的先导,由比尔·盖茨与保罗·艾伦创办于 1975 年,公司总部设立在华盛顿州的雷德蒙德(Redmond,邻近西雅图)。以研发、制造、授权和提供广泛的电脑软件服务业务为主。

    8 引用 • 44 回帖 • 1 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    694 引用 • 537 回帖 • 1 关注
  • Sandbox

    如果帖子标签含有 Sandbox ,则该帖子会被视为“测试帖”,主要用于测试社区功能,排查 bug 等,该标签下内容不定期进行清理。

    441 引用 • 1238 回帖 • 598 关注
  • Shell

    Shell 脚本与 Windows/Dos 下的批处理相似,也就是用各类命令预先放入到一个文件中,方便一次性执行的一个程序文件,主要是方便管理员进行设置或者管理用的。但是它比 Windows 下的批处理更强大,比用其他编程程序编辑的程序效率更高,因为它使用了 Linux/Unix 下的命令。

    125 引用 • 74 回帖
  • TextBundle

    TextBundle 文件格式旨在应用程序之间交换 Markdown 或 Fountain 之类的纯文本文件时,提供更无缝的用户体验。

    1 引用 • 2 回帖 • 82 关注
  • Chrome

    Chrome 又称 Google 浏览器,是一个由谷歌公司开发的网页浏览器。该浏览器是基于其他开源软件所编写,包括 WebKit,目标是提升稳定性、速度和安全性,并创造出简单且有效率的使用者界面。

    63 引用 • 289 回帖
  • Kotlin

    Kotlin 是一种在 Java 虚拟机上运行的静态类型编程语言,由 JetBrains 设计开发并开源。Kotlin 可以编译成 Java 字节码,也可以编译成 JavaScript,方便在没有 JVM 的设备上运行。在 Google I/O 2017 中,Google 宣布 Kotlin 成为 Android 官方开发语言。

    19 引用 • 33 回帖 • 85 关注
  • Swift

    Swift 是苹果于 2014 年 WWDC(苹果开发者大会)发布的开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。

    34 引用 • 37 回帖 • 558 关注
  • Vue.js

    Vue.js(读音 /vju ː/,类似于 view)是一个构建数据驱动的 Web 界面库。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。

    268 引用 • 666 回帖 • 2 关注
  • VirtualBox

    VirtualBox 是一款开源虚拟机软件,最早由德国 Innotek 公司开发,由 Sun Microsystems 公司出品的软件,使用 Qt 编写,在 Sun 被 Oracle 收购后正式更名成 Oracle VM VirtualBox。

    10 引用 • 2 回帖 • 16 关注
  • 新人

    让我们欢迎这对新人。哦,不好意思说错了,让我们欢迎这位新人!
    新手上路,请谨慎驾驶!

    52 引用 • 228 回帖