设计模式之 - 单例模式

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

设计模式详解。

《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();        }    }}

输出:

105995111910599511191059951119105995111910599511191059951119105995111910599511191059951119

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

方式 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 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • 设计模式

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

    200 引用 • 120 回帖

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • Unity

    Unity 是由 Unity Technologies 开发的一个让开发者可以轻松创建诸如 2D、3D 多平台的综合型游戏开发工具,是一个全面整合的专业游戏引擎。

    25 引用 • 7 回帖 • 174 关注
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    325 引用 • 1395 回帖
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    107 引用 • 295 回帖 • 1 关注
  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    334 引用 • 323 回帖 • 1 关注
  • 宕机

    宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。

    13 引用 • 82 回帖 • 52 关注
  • 996
    13 引用 • 200 回帖 • 6 关注
  • Openfire

    Openfire 是开源的、基于可拓展通讯和表示协议 (XMPP)、采用 Java 编程语言开发的实时协作服务器。Openfire 的效率很高,单台服务器可支持上万并发用户。

    6 引用 • 7 回帖 • 95 关注
  • Log4j

    Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。

    20 引用 • 18 回帖 • 31 关注
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 384 关注
  • V2Ray
    1 引用 • 15 回帖
  • etcd

    etcd 是一个分布式、高可用的 key-value 数据存储,专门用于在分布式系统中保存关键数据。

    5 引用 • 26 回帖 • 529 关注
  • PWA

    PWA(Progressive Web App)是 Google 在 2015 年提出、2016 年 6 月开始推广的项目。它结合了一系列现代 Web 技术,在网页应用中实现和原生应用相近的用户体验。

    14 引用 • 69 回帖 • 154 关注
  • H2

    H2 是一个开源的嵌入式数据库引擎,采用 Java 语言编写,不受平台的限制,同时 H2 提供了一个十分方便的 web 控制台用于操作和管理数据库内容。H2 还提供兼容模式,可以兼容一些主流的数据库,因此采用 H2 作为开发期的数据库非常方便。

    11 引用 • 54 回帖 • 654 关注
  • DNSPod

    DNSPod 建立于 2006 年 3 月份,是一款免费智能 DNS 产品。 DNSPod 可以为同时有电信、网通、教育网服务器的网站提供智能的解析,让电信用户访问电信的服务器,网通的用户访问网通的服务器,教育网的用户访问教育网的服务器,达到互联互通的效果。

    6 引用 • 26 回帖 • 510 关注
  • 微软

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

    8 引用 • 44 回帖
  • Firefox

    Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。

    8 引用 • 30 回帖 • 407 关注
  • HBase

    HBase 是一个分布式的、面向列的开源数据库,该技术来源于 Fay Chang 所撰写的 Google 论文 “Bigtable:一个结构化数据的分布式存储系统”。就像 Bigtable 利用了 Google 文件系统所提供的分布式数据存储一样,HBase 在 Hadoop 之上提供了类似于 Bigtable 的能力。

    17 引用 • 6 回帖 • 73 关注
  • Sphinx

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

    1 引用 • 211 关注
  • Gitea

    Gitea 是一个开源社区驱动的轻量级代码托管解决方案,后端采用 Go 编写,采用 MIT 许可证。

    4 引用 • 16 回帖 • 4 关注
  • DevOps

    DevOps(Development 和 Operations 的组合词)是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。

    47 引用 • 25 回帖
  • Solidity

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

    3 引用 • 18 回帖 • 399 关注
  • NetBeans

    NetBeans 是一个始于 1997 年的 Xelfi 计划,本身是捷克布拉格查理大学的数学及物理学院的学生计划。此计划延伸而成立了一家公司进而发展这个商用版本的 NetBeans IDE,直到 1999 年 Sun 买下此公司。Sun 于次年(2000 年)六月将 NetBeans IDE 开源,直到现在 NetBeans 的社群依然持续增长。

    78 引用 • 102 回帖 • 680 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    142 引用 • 442 回帖
  • Vue.js

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

    266 引用 • 665 回帖 • 1 关注
  • Wide

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

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

    30 引用 • 218 回帖 • 629 关注
  • 安全

    安全永远都不是一个小问题。

    199 引用 • 816 回帖
  • 程序员

    程序员是从事程序开发、程序维护的专业人员。

    567 引用 • 3532 回帖