一、前言
由于工作原因,导致我断了一阵子的设计模式学习,今天我们来继续设计模式的学习。今天我们要了解的是单例模式——单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
二、单例模式详解
1、定义
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
这种模式的类,类负责创建自己的对象,同时确保只有单个对象被创建。并且提供一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
2、介绍
- 意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点
- 主要解决: 一个全局使用的类频繁地创建与销毁
- 何时使用: 当您想控制实例数目,节省系统资源的时候
- 如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建
- 关键代码: 构造函数是私有的
2.1、特点
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
2.2、应用实例
- 一个班级只有一个班主任
- Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行
- 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件
2.3、优点
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)
- 避免对资源的多重占用(比如写文件操作)
2.4、缺点
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
- 也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难
2.5、使用场景
- 要求生产唯一序列号
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等
3、UML 图
示例
entity
public class SingleObject {
//创建 SingleObject 的一个对象
private static SingleObject instance = new SingleObject();
//让构造函数为 private,这样该类就不会被实例化
private SingleObject(){}
//获取唯一可用的对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
demo
public class SingletonPatternDemo {
public static void main(String[] args) {
//不合法的构造函数
//编译时错误:构造函数 SingleObject() 是不可见的
//SingleObject object = new SingleObject();
//获取唯一可用的对象
SingleObject object1 = SingleObject.getInstance();
SingleObject object2 = SingleObject.getInstance();
System.out.println(object1);
System.out.println(object2);
}
}
结果
4、单例模式 VS 静态类
在知道了什么是单例模式后,我想小伙伴们一定会想到静态类,说:“既然只使用一个对象,为何不干脆使用静态类?”,这里我会将单例模式和静态类进行一个比较。
- 单例可以继承和被继承,方法可以被 override,而静态方法不可以
- 静态方法中产生的对象会在执行后被释放,进而被 GC 清理,不会一直存在于内存中
- 静态类会在第一次运行时初始化,单例模式可以有其他的选择,即可以延迟加载
- 基于 2, 3 条,由于单例对象往往存在于 DAO 层(例如 sessionFactory),如果反复的初始化和释放,则会占用很多资源,而使用单例模式将其常驻于内存可以更加节约资源
- 静态方法有更高的访问效率
- 单例模式很容易被测试
5、单例模式的几种实现方式
5.1、饿汉式
public class HungrySingle {
private final static HungrySingle instance = new HungrySingle();
private HungrySingle() {
System.out.println(Thread.currentThread().getName());
}
public static HungrySingle getInstance() {
return instance;
}
}
歩骤
- 构造器私有
- 创建内部对象
- 写一个静态公共方法
优点
- 这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题
- 没有任何锁,执行效率高,用户体验比懒汉式单例模式更好
缺点
- 在类装载的时候就完成实例化,没有达到 Lazy Loading(延迟加载) 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
- 这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果
建议
- 适用于单例模式较少的场景
- 如果我们在程序启动后,一定会加载到类,那么用饿汉模式实现的单例简单又实用
- 如果我们是写一些工具类,则优先考虑使用懒汉模式,可以避免提前被加载到内存中,占用系统资源
5.2、懒汉式
懒汉式(普通,线程不安全)
public class UnsafeLazySingle {
private static UnsafeLazySingle instance;
private UnsafeLazySingle() {
System.out.println(Thread.currentThread().getName());
}
public static UnsafeLazySingle getInstance() {
if (null == instance) {
instance = new UnsafeLazySingle();
}
return instance;
}
}
优点
- 起到了 Lazy Loading(延迟加载)的效果,但是只能在单线程下使用
缺点
- 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式
建议
- 多线程不能用
懒汉式(同步方法,线程安全)
class LazySingleF{
private static LazySingleF instance;
private LazySingleF(){
System.out.println(Thread.currentThread().getName());
}
public static synchronized LazySingleF getInstance() {
if (instance == null) {
instance = new LazySingleF();
}
return instance;
}
}
优点
- 解决了线程安全问题
缺点
- 效率太低了,每个线程在想获得类的实例时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了
建议
- 在实际开发中,不推荐使用这种方式
懒汉式(同步代码块,线程不安全)
class LazySingleB{
private static LazySingleB instance;
private LazySingleB() {
System.out.println(Thread.currentThread().getName());
}
public static LazySingleB getInstance() {
if (instance == null) {
synchronized (LazySingleB.class) {
instance = new LazySingleB();
}
}
return instance;
}
}
缺点
- 不能线程同步。假如一个线程进入了 if (instance == null),还未来得及执行,另一个线程也通过了这个判断语句,会产生多个实例
- 效率很低
建议
- 多线程不能使用
懒汉式(双重检查,线程安全)(Double Check Lock(DCL))
class LazySingleD{
private volatile static LazySingleD instance;
private LazySingleD() {
System.out.println(Thread.currentThread().getName());
}
public static LazySingleD getInstance() {
if (null == instance) {
synchronized (LazySingleD.class) {
if (instance ==null) {
instance = new LazySingleD();
}
}
}
return instance;
}
}
优点
- 线程安全;延迟加载;效率较高
- Double-Check Lock 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步。
缺点
- 相比单锁而言,双重检查锁性能上虽然有提升,但是依旧用到了 synchronized 关键字总归要上锁,对程序性能还是存在一定的性能影响。注意里面 volatile 的使用!!!
建议
- 在实际开发中,推荐使用这种单例设计模式
注意
由于 jvm 存在乱序执行功能,DCL 也会出现线程不安全的情况。具体分析如下:
INSTANCE = new SingleTon();
这个步骤,其实在 jvm 里面的执行分为三步:
- 在堆内存开辟内存空间
- 在堆内存中实例化 SingleTon 里面的各个参数
- 把对象指向堆内存空间
由于 jvm 存在乱序执行功能,所以可能在 2 还没执行时就先执行了 3,如果此时再被切换到线程 B 上,由于执行了 3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的 DCL 失效问题。
不过在 JDK1.5 之后,官方也发现了这个问题,故而具体化了 volatile,即在 JDK1.6 及以后,只要定义为
private volatile static LazySingleD instance;
就可解决 DCL 失效问题。volatile 的内存栅栏功能,告知编译器的在标记的变量前后不使用优化功能,禁止指令重排序。
5.3、内部静态类
class InnerClass {
private InnerClass() {
System.out.println(Thread.currentThread().getName());
}
private static class InnerClassHolder {
private static InnerClass instance = new InnerClass();
}
public static InnerClass getInstance() {
return InnerClassHolder.instance;
}
}
优点
- 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化 INSTANCE,故而不占内存
- 当 InnerClass 第一次被加载时,并不需要去加载 SingleTonHoler,只有当 getInstance()方法第一次被调用时,才会去初始化 INSTANCE,第一次调用 getInstance()方法会导致虚拟机加载 SingleTonHoler 类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
缺点
- 静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去
建议
- 屏蔽饿汉式单例模式的内存浪费问题和双重检查锁中 synchronized 的性能问题,同时考虑避免因为反射破坏单例问题,相对而言性能最好!
补充知识——类加载
JAVA 虚拟机在有且仅有的 5 种场景下会对类进行初始化:
-
遇到 new、getstatic、setstatic 或者 invokestatic 这 4 个字节码指令时,对应的 java 代码场景为:
new一个关键字或者一个实例化对象时 读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外) 调用一个类的静态方法时。
-
使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
-
当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的类),虚拟机会先初始化这个类。
-
当使用 JDK 1.7 等动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
以上是类的主动引用。除此之外,所有引用类都不会对类进行初始化,称为被动引用。
静态内部类就属于被动引用的行列,为什么?
我们再回头看下 getInstance()方法,调用的是 InnerClassHolder.instance,取的是 InnerClassHolder 里的 instance 对象,跟上面那个 DCL 方法不同的是,getInstance()方法并没有多次去 new 对象,故不管多少个线程去调用 getInstance()方法,取的都是同一个 instance 对象,而不用去重新创建。
当 getInstance()方法被调用时,InnerClassHolder 才在 InnerClass 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 instance 也真正被创建,然后再被 getInstance()方法返回出去,这点同饿汉模式。
那么 instance 在创建过程中又是如何保证线程安全的呢?
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。
如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。
可以看出 INSTANCE 在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
5.4、枚举
enum Enums {
/**
* 实例对象
*/
INSTANCE;
/**
* 两数相加之和方法
* @param a
* @param b
* @return
*/
public int add(int a, int b)
{
return a + b;
}
}
优点
- 不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
- 直接通过 Enums.INSTANCE.add()的方式调用即可。方便、简洁又安全
三、总结
以上就是我个人关于 设计模式——单例模式 的一些笔记,如果有什么问题,可以将问题发我邮箱 luodiab@126.com ,欢迎各位的意见。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于