透彻理解单例模式

简介: 主要内容有:该模式的介绍,包括:引子、意图(大白话解释)类图、时序图(理论规范)该模式的代码示例:熟悉该模式的代码长什么样子该模式的优缺点:模式不是万金油,不可以滥用模式该模式的实际使用案例:了解它在哪些重要的源码中被使用


前言



主要内容有:

  • 该模式的介绍,包括:
  • 引子、意图(大白话解释)
  • 类图、时序图(理论规范)
  • 该模式的代码示例:熟悉该模式的代码长什么样子
  • 该模式的优缺点:模式不是万金油,不可以滥用模式
  • 该模式的实际使用案例:了解它在哪些重要的源码中被使用


创建型——单例模式



引子

《HEAD FIRST设计模式》中“单例模式”又称为“单件模式”

对于系统中的某些类来说,只有一个实例很重要。比如大家熟悉的Spring框架中,Controller和Service都默认是单例模式。

如果用生活中的例子举例,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。

如何保证一个类只有一个实例并且这个实例易于被访问呢?

答:定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。


意图

确保一个类只有一个实例,并提供该实例的全局访问点。

单例模式的要点有三个:

  • 一是某个类只能有一个实例;
  • 二是它必须自行创建这个实例;
  • 三是它必须自行向整个系统提供这个实例。

使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。

私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。


类图

如果看不懂UML类图,可以先粗略浏览下该图,想深入了解的话,可以继续谷歌,深入学习:

单例模式的类图:


时序图

时序图(Sequence Diagram)是显示对象之间交互的图,这些对象是按时间顺序排列的。时序图中显示的是参与交互的对象及其对象之间消息交互的顺序。

我们可以大致浏览下时序图,如果感兴趣的小伙伴可以去深究一下:


实现



单例模式有非常多的实现方式,这里我们从最差的实现方式逐渐过渡到优雅的实现方式(剑指offer的方式),包括:

  • 懒汉式-线程不安全
  • 饿汉式-线程安全
  • 懒汉式-线程安全
  • 懒汉式(延迟实例化)—— 线程安全/双重校验 (重要,牢记)
  • 静态内部类实现
  • 枚举实现 (重要,牢记)


1. 懒汉式-线程不安全

以下实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。

这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将导致实例化多次 uniqueInstance。

public class Singleton {
    private static Singleton uniqueInstance;
    private Singleton() {
    }
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
复制代码

2. 饿汉式-线程安全

如此一来,只会实例化一次,作为静态变量

private static Singleton uniqueInstance = new Singleton();
复制代码

3. 懒汉式(延迟实例化)—— 线程安全

只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。

但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。

public static synchronized Singleton getUniqueInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}
复制代码

4. 懒汉式(延迟实例化)—— 线程安全/双重校验

一.私有化构造函数

二.声明静态单例对象

三.构造单例对象之前要加锁(lock一个静态的object对象)或者方法上加synchronized。

四.需要两次检测单例实例是否已经被构造,分别在锁之前和锁之后

使用lock(obj)

public class Singleton {  
    private Singleton() {}                     //关键点0:构造函数是私有的
    private volatile static Singleton single;    //关键点1:声明单例对象是静态的
    private static object obj= new object();
    public static Singleton GetInstance()      //通过静态方法来构造对象
    {                        
         if (single == null)                   //关键点2:判断单例对象是否已经被构造
         {                             
            lock(obj)                          //关键点3:加线程锁
            {
               if(single == null)              //关键点4:二次判断单例是否已经被构造
               {
                  single = new Singleton();  
                }
             }
         }    
        return single;  
    }  
}
复制代码

使用synchronized (Singleton.class)

public class Singleton {
    private Singleton() {}
    private volatile static Singleton uniqueInstance;
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
复制代码
面试时可能的提问

0.为何要检测两次?

答:如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在if语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton(); 这条语句,只是先后的问题,也就是说会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。

1.构造函数能否公有化?

答:不行,单例类的构造函数必须私有化,单例类不能被实例化,单例实例只能静态调用。

2.lock住的对象为什么要是object对象,可以是int吗?

答:不行,锁住的必须是个引用类型。如果锁值类型,每个不同的线程在声明的时候值类型变量的地址都不一样,那么上个线程锁住的东西下个线程进来会认为根本没锁。

3.uniqueInstance 采用 volatile 关键字修饰

uniqueInstance = new Singleton(); 这段代码其实是分为三步执行。

分配内存空间
初始化对象
将 uniqueInstance 指向分配的内存地址
复制代码

但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1-->3-->2

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(uniqueInstance == null){
        // B线程检测到uniqueInstance不为空
            synchronized(Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                    // A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。
                }
            }
        }
        return uniqueInstance;// 后面B线程执行时将引发:对象尚未初始化错误。
    }
}
复制代码

所以B线程检测到不为null后,直接出去调用该单例,而A还没有运行完构造函数,导致该单例还没创建完毕,B调用会报错!所以必须用volatile防止JVM重排指令

5. 静态内部类实现

当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例。

这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。

public class Singleton {
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}
复制代码

6. 枚举实现

这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。

public enum Singleton {
    INSTANCE;
    private String objName;
    public String getObjName() {
        return objName;
    }
    public void setObjName(String objName) {
        this.objName = objName;
    }
    public static void main(String[] args) {
        // 单例测试
        Singleton firstSingleton = Singleton.INSTANCE;
        firstSingleton.setObjName("firstName");
        System.out.println(firstSingleton.getObjName());
        Singleton secondSingleton = Singleton.INSTANCE;
        secondSingleton.setObjName("secondName");
        System.out.println(firstSingleton.getObjName());
        System.out.println(secondSingleton.getObjName());
        // 反射获取实例测试
        try {
            Singleton[] enumConstants = Singleton.class.getEnumConstants();
            for (Singleton enumConstant : enumConstants) {
                System.out.println(enumConstant.getObjName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码
为什么枚举是单例模式的最好方式?

考虑以下单例模式的实现,该 Singleton 在每次序列化的时候都会创建一个新的实例,为了保证只创建一个实例,必须声明所有字段都是 transient,并且提供一个 readResolve() 方法。

public class Singleton implements Serializable {
    private static Singleton uniqueInstance;
    private Singleton() {
    }
    public static synchronized Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
复制代码

如果不使用枚举来实现单例模式,会出现反射攻击,因为通过反射的setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象。

枚举实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。

从上面的讨论可以看出,解决序列化和反射攻击很麻烦,而枚举实现不会出现这两种问题,所以说枚举实现单例模式是最佳实践。


使用场景举例



  • Logger类,全局唯一,保证你能在每个类里调用为一个Logger输出日志
  • Spring:Spring里很多类都是单例的,也是你理解单例最合适的地方,比如Controller和Service类,默认都是单例的。
  • 数据库连接池对象:你从代码的任何地方都需要拿到连接池里的资源。


参考



相关文章
|
1月前
|
设计模式 安全 Java
Java编程中的单例模式:理解与实践
【10月更文挑战第31天】在Java的世界里,单例模式是一种优雅的解决方案,它确保一个类只有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的实现方式、使用场景及其优缺点,同时提供代码示例以加深理解。无论你是Java新手还是有经验的开发者,掌握单例模式都将是你技能库中的宝贵财富。
38 2
|
设计模式 安全
面试最常见的设计模式之单例模式
面试最常见的设计模式之单例模式
152 1
|
设计模式 安全 Java
深入浅出 - 单例模式
深入浅出 - 单例模式
80 0
深入浅出 - 单例模式
|
设计模式 SQL 安全
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)
349 0
【设计模式学习笔记】单例模式详解(懒汉式遇上多线程问题解析基于C++实现)
|
设计模式 存储 安全
我终于读懂了单例模式。。。
我终于读懂了单例模式。。。
我终于读懂了单例模式。。。
|
设计模式 安全 Java
面试基础篇——单例模式(一)
面试基础篇——单例模式
110 0
|
设计模式 Java
面试基础篇——单例模式(二)
面试基础篇——单例模式
90 0
|
设计模式 安全 Java
设计模式轻松学【二】你想学的单例模式都在这
一个类仅有一个实例,由自己创建并对外提供一个实例获取的入口,外部类可以通过这个入口直接获取该实例对象。
138 0
设计模式轻松学【二】你想学的单例模式都在这
|
安全
【面试:基础篇09:单例模式全总结】
【面试:基础篇09:单例模式全总结】
94 0
|
设计模式 Java 容器
84. 面试中设计模式能问些什么?比如说一下三种单例模式实现
84. 面试中设计模式能问些什么?比如说一下三种单例模式实现
109 0