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类图来做个介绍。
单例模式通用写法:
3、单例模式的优缺点和使用场景
在很多技术的学习和选择的前置条件我想可能都会是这两个问题为先驱,所以把本该放置到最后去总结的两个点,优先提到了最前面,也正是出于这个原因。单例模式的编码说简单确实简单,编码一个最优的单例或者如何更好的适用当前场景,对编码人员有一定的要求,如下主要分析其优缺点和使用场景,便于编码人员的选择。
3.1 单例模式的优点
只创建一个实例对象,减少内存开销,同时也减少了GC
只创建一个示例对象,通过全局唯一的访问点,可以避免对资源的多重占用,例如文件的对同一个文件的写
通过这个全局访问点为抓手,可以对资源的访问做优化处理,例如访问点中预置缓存
3.2 单例模式的缺点
单例模式的功能一般写于一个类中,这回导致业务逻辑耦合,如果设计不合理,是违背单一职责原则
单例模式一般不实现接口,使其扩展困难,是违背开闭原则
单例模式只存在一个对象,在并发测试中不利于调试
3.3 使用典型场景
日志类,对于日志的记录等操作,我们通常使用单例模式
数据库连接和访问的管理
文件资源访问管理
生成唯一的序列号
系统全局唯一的访问端点,例如系统访问请求计数器
对象的创建需要大量的开销或者对象会被频繁调用可以考虑通过单例模式来优化替换
3.4 源码中的使用
说三道四不如看看Java最佳规范JDK中对于单例模式的一些使用
java.lang.Runtime
除了在jdk源码中对于单例模式有不少的使用之外,我们最常见的Spring框架中,每个Bean默认就是单例的,这样做的原因在于Spring容器可以管理Bean的生命周期。修改Spring中Bean的单例属性,只需要设置为Prototype类型即可,这样Bean的生命周期将不再有Spring容器跟踪。
4、饿汉式单例
饿汉式单例主要体现在一个饿字,也就是说在使用这个对象之前,在类加载的时候就立即初始化,创建其实例对象。这样做的好处是线程安全、使用时速度更快。饿汉
写法一:
饿汉式单例可以保证线程安全,执行效率高,同时编码简单容易理解。但是饿汉式单例只适用于单例对象减少的情况,如果大量编写饿汉式单例不仅会给系统启动带来负担,也可能会导致内存的浪费,比如创建的单例对象在程序运行过程中并未使用。因此这对内存浪费这个问题,我们衍生出了懒汉式单例。
5、懒汉式单例
懒汉式单例主要体现在一个懒字,也就是说类加载的时候我懒得实例化,等你需要用了再说吧!接下来的懒汉式单例中我通过一步步的优化和推翻来演进如何编写一个优秀的单例。
5.1 普通懒汉式单例
这是一个非常普通的懒汉式单例模式的写法,相信很多初学者会编码成这样,但其实这是一种错误的线程不安全的写法。我们通过一个断点测试来证明其不安全,我采用的是IDEA编写代码,通过设置断点为Thread模式来使得线程1和线程2同时满足LAZY_SINGLETON == null。
测试代码如上代码看似兼顾了性能和同步对象的实例化,但是这里的同步语言并不能保证对象只实例化一次,它只能保证每次只有一个线程在实例化这个对象。试想一下,如果线程1和线程2均执行到代码21行,此时线程1获得锁继续执行,实例化对象LazySingletonDemo4,当线程1退出锁后,线程2获取到锁,线程2也会对LazySingletonDemo4进行实例化,这种情况也可以用上面的断点法测试出来。所以这种情况是错误的,但是只有小小的改动一下即可,改动后的方式如下所示。
5.3 双重检查锁懒汉式单例
双重检查锁看名字高大上,其实就是在上面的LazySingletonDemo4多个if判断而已,理解为双重检查+锁可能更明了,其代码如下所示:
这三条指令中,指令2和指令3可能会出现重排序,也就是说对象的初始化会被后置到将分配的地址指向对象的引用这个指令的后面;这种重排序,假设是在线程1执行中发生了,此时线程2执行到第20行 if (LAZY_SINGLETON == null),此时LAZY_SINGLETON 并不为null,程序会直接返回LAZY_SINGLETON对象,但是此时的对象是一个实例化不完全的对象,这种情况是不允许存在的。
其解决办法是通过volatile关键字借助其内存语义来禁止指令重排序,这样2和3指令之间的重排序将会被禁止,这涉及到JMM规范,需要的请查看我的并发专题系列文章。其改造代码如下所示:
package com.lizba.pattern.singleton.lazy;
5.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(); } }
更加上述输出结果,可以非常明确的发现,instance1和instance2就是同一个对象,那么为何枚举可以实现单例呢?为何其被Joshua Bloch大师称为最好的单例模式编码方式呢?
我们通过反编译工具jad来一探究竟,首先使用jad,在EnumSingletonDemo.class所在的目录下执行jad EnumSingletonDemo.class,这样会生成一个EnumSingletonDemo.jad文件;生成的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 }); } }