单例模式(万字长文精讲)

简介: 单例模式(万字长文精讲)

1、单例模式的定义

单例模式(Singleton Pattern),确保一个类只有一个实例,并提供对它的全局访问点。这是在java-design-patterns.com中对于单例模式的定义,其原文定义如下:


Ensure a class has only one instance, and provide a global point of acess to it.


简单来说就是确保系统中只创建特定类的一个对象(全文的重点和围绕展开的都是如何安全的更高效的去将类的实例化限制为一个对象)。


2、单例模式的简单使用

在深入分析单例模式之前,先简单的有个整体的概念,单例模式大致是怎么使用的。我们通过一个简单的demo和其UML类图来做个介绍。

单例模式通用写法:

image.pngimage.png3、单例模式的优缺点和使用场景

在很多技术的学习和选择的前置条件我想可能都会是这两个问题为先驱,所以把本该放置到最后去总结的两个点,优先提到了最前面,也正是出于这个原因。单例模式的编码说简单确实简单,编码一个最优的单例或者如何更好的适用当前场景,对编码人员有一定的要求,如下主要分析其优缺点和使用场景,便于编码人员的选择。


3.1 单例模式的优点

只创建一个实例对象,减少内存开销,同时也减少了GC

只创建一个示例对象,通过全局唯一的访问点,可以避免对资源的多重占用,例如文件的对同一个文件的写

通过这个全局访问点为抓手,可以对资源的访问做优化处理,例如访问点中预置缓存

3.2 单例模式的缺点

单例模式的功能一般写于一个类中,这回导致业务逻辑耦合,如果设计不合理,是违背单一职责原则

单例模式一般不实现接口,使其扩展困难,是违背开闭原则

单例模式只存在一个对象,在并发测试中不利于调试

3.3 使用典型场景

日志类,对于日志的记录等操作,我们通常使用单例模式

数据库连接和访问的管理

文件资源访问管理

生成唯一的序列号

系统全局唯一的访问端点,例如系统访问请求计数器

对象的创建需要大量的开销或者对象会被频繁调用可以考虑通过单例模式来优化替换

3.4 源码中的使用

说三道四不如看看Java最佳规范JDK中对于单例模式的一些使用


java.lang.Runtime

image.pngimage.png除了在jdk源码中对于单例模式有不少的使用之外,我们最常见的Spring框架中,每个Bean默认就是单例的,这样做的原因在于Spring容器可以管理Bean的生命周期。修改Spring中Bean的单例属性,只需要设置为Prototype类型即可,这样Bean的生命周期将不再有Spring容器跟踪。


4、饿汉式单例

饿汉式单例主要体现在一个饿字,也就是说在使用这个对象之前,在类加载的时候就立即初始化,创建其实例对象。这样做的好处是线程安全、使用时速度更快。饿汉

写法一:

image.pngimage.png饿汉式单例可以保证线程安全,执行效率高,同时编码简单容易理解。但是饿汉式单例只适用于单例对象减少的情况,如果大量编写饿汉式单例不仅会给系统启动带来负担,也可能会导致内存的浪费,比如创建的单例对象在程序运行过程中并未使用。因此这对内存浪费这个问题,我们衍生出了懒汉式单例。


5、懒汉式单例

懒汉式单例主要体现在一个懒字,也就是说类加载的时候我懒得实例化,等你需要用了再说吧!接下来的懒汉式单例中我通过一步步的优化和推翻来演进如何编写一个优秀的单例。


5.1 普通懒汉式单例

image.png这是一个非常普通的懒汉式单例模式的写法,相信很多初学者会编码成这样,但其实这是一种错误的线程不安全的写法。我们通过一个断点测试来证明其不安全,我采用的是IDEA编写代码,通过设置断点为Thread模式来使得线程1和线程2同时满足LAZY_SINGLETON == null。

测试代码image.pngimage.pngimage.pngimage.pngimage.pngimage.png如上代码看似兼顾了性能和同步对象的实例化,但是这里的同步语言并不能保证对象只实例化一次,它只能保证每次只有一个线程在实例化这个对象。试想一下,如果线程1和线程2均执行到代码21行,此时线程1获得锁继续执行,实例化对象LazySingletonDemo4,当线程1退出锁后,线程2获取到锁,线程2也会对LazySingletonDemo4进行实例化,这种情况也可以用上面的断点法测试出来。所以这种情况是错误的,但是只有小小的改动一下即可,改动后的方式如下所示。


5.3 双重检查锁懒汉式单例

双重检查锁看名字高大上,其实就是在上面的LazySingletonDemo4多个if判断而已,理解为双重检查+锁可能更明了,其代码如下所示:

image.png这三条指令中,指令2和指令3可能会出现重排序,也就是说对象的初始化会被后置到将分配的地址指向对象的引用这个指令的后面;这种重排序,假设是在线程1执行中发生了,此时线程2执行到第20行 if (LAZY_SINGLETON == null),此时LAZY_SINGLETON 并不为null,程序会直接返回LAZY_SINGLETON对象,但是此时的对象是一个实例化不完全的对象,这种情况是不允许存在的。

其解决办法是通过volatile关键字借助其内存语义来禁止指令重排序,这样2和3指令之间的重排序将会被禁止,这涉及到JMM规范,需要的请查看我的并发专题系列文章。其改造代码如下所示:


package com.lizba.pattern.singleton.lazy;

image.pngimage.pngimage.pngimage.png5.4 静态内部类懒汉式单例

在双重检查锁的演进中,我们通过不断的缩小锁的范围,以及对对象是否未实例化做了两次判断,最后对反射获取对象这种操作做了处理;但是归根到底,双重检查锁中有个锁字,就难规避性能讨论的问题,其实这种问题在大部分场景中是可以接受;如果硬要寻求一种既是懒汉式,又不需要锁的单例模式,那么通过静态内部类的加载特性,巧妙的实现懒汉式单例模式会是一种不错的选择。

Java语言中的内部类是延时加载的,只有在第一次使用的时候才会被加载,不使用则不加载。

我们通过该特性,编码的懒汉式单例如下所示:

package com.lizba.pattern.singleton.lazy;
/**
 * <p>
 *      内部类懒汉式单例
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/27 20:14
 */
public class LazySingletonDemo8 {
    private LazySingletonDemo8() {
        if (LazyHolderInner.LAZY_SINGLETON != null) {
            throw new RuntimeException("This operation is forbidden.");
        }
    }
    public static LazySingletonDemo8 getInstance() {
        return LazyHolderInner.LAZY_SINGLETON;
    }
    /**
     *  使用内部类,被使用才加载的特性来
     */
    private static class LazyHolderInner {
      public static final LazySingletonDemo8 LAZY_SINGLETON = new LazySingletonDemo8();
    }
}

image.pngimage.png更加上述输出结果,可以非常明确的发现,instance1和instance2就是同一个对象,那么为何枚举可以实现单例呢?为何其被Joshua Bloch大师称为最好的单例模式编码方式呢?

我们通过反编译工具jad来一探究竟,首先使用jad,在EnumSingletonDemo.class所在的目录下执行jad EnumSingletonDemo.class,这样会生成一个EnumSingletonDemo.jad文件;image.png生成的EnumSingletonDemo.jad文件内容如下:我们看到代码的最后有一个静态代码块,在这里一看便知。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   EnumSingletonDemo.java
package com.lizba.pattern.singleton.hungry;
public final class EnumSingletonDemo extends Enum{
    public static EnumSingletonDemo[] values(){
        return (EnumSingletonDemo[])$VALUES.clone();
    }
    public static EnumSingletonDemo valueOf(String name){
        return (EnumSingletonDemo)Enum.valueOf(com/lizba/pattern/singleton/hungry/EnumSingletonDemo, name);
    }
    private EnumSingletonDemo(String s, int i) {
        super(s, i);
    }
    public Object getObject()  {
        return object;
    }
    public void setObject(Object object){
        this.object = object;
    }
    public static EnumSingletonDemo getInstance() {
        return SINGLETON_INSTANCE;
    }
    public static final EnumSingletonDemo SINGLETON_INSTANCE;
    private Object object;
    private static final EnumSingletonDemo $VALUES[];
    // 静态代码块中实例化了EnumSingletonDemo
    static  {
        SINGLETON_INSTANCE = new EnumSingletonDemo("SINGLETON_INSTANCE", 0);
        $VALUES = (new EnumSingletonDemo[] {
            SINGLETON_INSTANCE
        });
    }
}

image.pngimage.png

目录
相关文章
|
设计模式 算法 Java
万字长文-设计模式(建议先收藏,再观看)
万字长文-设计模式(建议先收藏,再观看)
|
7月前
|
存储 缓存 NoSQL
揭秘一线大厂Redis面试高频考点(3万字长文、吐血整理)
揭秘一线大厂Redis面试高频考点(3万字长文、吐血整理)
594 5
揭秘一线大厂Redis面试高频考点(3万字长文、吐血整理)
|
7月前
|
Java 数据库连接 数据库
【万字长文】Java面试八股文:深入剖析常见问题与解答
【万字长文】Java面试八股文:深入剖析常见问题与解答
1253 0
|
消息中间件 分布式计算 Kafka
Spark面试干货总结!(8千字长文、27个知识点、21张图)
Spark面试干货总结!(8千字长文、27个知识点、21张图)
314 1
|
设计模式 存储 缓存
关于常见设计模式的那些事 | 2.5W字长文
在软件开发中,设计模式是一种被广泛应用的方法论,它们是在具有良好面向对象设计基础上的可重用解决方案,它可以提高代码的可重用性、可扩展性和可维护性。设计模式不仅可以让我们更快地开发出高质量的软件,而且还可以让我们更好地理解已有的代码,从而更好地进行重构和维护。
658 0
|
机器学习/深度学习 缓存 架构师
十年技术进阶路,让我明白了三件要事(8000字长文)
8000字长文,大概花费您10分钟的阅读时间,我将以十年成长道路与八年写博经历分享与你,希望能给予各位读者或多或少的建议与提醒
308 1
十年技术进阶路,让我明白了三件要事(8000字长文)
|
存储 缓存 网络协议
前端面试基础网络问题(万字长文)
这篇文章你会了解到什么 • OSI七层协议和TCP/IP四层协议模型 • 什么是UDP协议 • 什么是TCP协议 • TCP连接过程 • TCP两次握手就可以完成,为撒需要三次了? • TCP断开连接过程 • 为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态? • ARQ协议(超时重传机制):停止等待ARQ协议、连续ARQ协议 • 滑动窗口协议 • 拥塞处理过程 • 拥塞算法
244 0
|
Java 程序员
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (4)
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (4)
112 0
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (4)
|
Java
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (1)
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (1)
133 0
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (1)
|
Java 索引
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (3)
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (3)
147 0
why哥被阿里一道基础面试题给干懵了,一气之下写出万字长文。 (3)