结合Spring源码学习单例设计模式

之前我学习了 Spring Ioc,明白了 Spring IoC 容器是一个管理Bean的容器,在Spring的定义中,它要求所有的IoC容器都需要实现接口 BeanFactory ,它是一个顶级容器接口。

BeanFactory.java源码

package org.springframework.beans.factory;

import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

public interface BeanFactory {

	// 前缀:用于取消引用FactoryBean实例并将其与FactoryBean创建的bean区别。
	String FACTORY_BEAN_PREFIX = "&";

	// 多个getBean方法:返回指定bean的实例,该实例可以是共享的,也可以是独立的。
	Object getBean(String name) throws BeansException;
    
	<T> T getBean(String name, Class<T> requiredType) throws BeansException;

	Object getBean(String name, Object... args) throws BeansException;

	<T> T getBean(Class<T> requiredType) throws BeansException;

	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

	// 多个getBeanProvider()方法:返回指定bean的提供者,允许延迟按需检索实例,包括可用性和唯一性选项。
	<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);

	<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

	// 通过 bean 的名字检查这个bean工厂是否包含一个bean定义或外部注册的单例
	boolean containsBean(String name);

	// 检查 bean 是否是单例
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

	// 检查 bean 是否是原型
	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

	// 多个isTypeMatch()方法:检查具有给定名称的bean是否与指定的类型匹配
	boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;

	boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

	// 多个getType()方法:确定具有给定名称的 bean 的类型
	@Nullable
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;

	@Nullable
	Class<?> getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException;

	// 返回给定 bean 名称的别名
	String[] getAliases(String name);

}

其中有多个 getBean 方法(实现过程源码解析可以参考这篇博文:点击查看,因为比较复杂,这里省略),这是 Spring IoC 容器最终要的方法之一,用来从 IoC 容器中获取 Bean。从这些方法中可以看到有按类型获取 Bean 的,也有按照名称获取 Bean 的,这些对Spring 的依赖注入是十分重要的。

另外这里有个检查是否是单例或者原型的方法,分别是 isSingletonisPrototype 方法。isSingleton 方法判断 Bean 是否在 Spring IoC中为单例。默认情况下,Bean 都是以单例存在的,也就是使用 getBean 方法返回的都是同一个对象。与 isSingleton 相反的就是 isPrototype 方法。

Spring 中是如何实现保存并提供单例的呢?接下来这里讨论与单例相关的知识点,重点是单例设计模式

一、单例类与原型类

在面向对象程序编程,比如java中,对象的产生是通过 new 关键字完成的(当然也有对象复制和反射,这些技术比较复杂,暂时忽略)。

  • 一个类可以通过 new 关键字实例化多个对象(实例),这个类就是原型的。

  • 一个类只允许一个对象(实例),这个类就是单例的。

这里要控制创建对象的操作,该怎么控制呢?

答案是利用构造方法,也叫构造器或者构造函数。

每个Java类都必须有构造方法,创建对象是通过类的构造方法来产生的。如果一个类中没有显式定义构造方法,那么Java编译器会自动为该类加上一个空的无形参构造方法,如果有构造方法了,那么编译器就不会自动加上构造方法。构造方法可以重载,使用 new 关键字创建对象时,都会根据输入的参数调用相应的构造方法。默认构造方法格式如下:public 类名 () {}如果手动添加了构造器,那么默认构造器就会消失,而且如果把构造函数设置为私有的(private)访问权限,就可以禁止外部创建对象了,避免被其他类 new 出来一个对象。

比如鸟类,不给外部创建对象,那么可以这样:

public class Bird {
    // 私有的创建对象构造方法,外部无法创建对象实例
    private Bird(){ }
}

为了给外部提供类对象,就需要自行创建一个对象并提供一个访问的方法:

public class Bird {
	private static final Bird bird = new Bird();

    // 私有的创建对象构造方法,外部无法创建对象实例
    private Bird(){ }
    
   	// 对外提供对象的方法
    public static Bird getInstance () {
        return bird;
    }
}

注:static变量是随着类被初次访问而初始化的,static可以保证在一个线程未使用其他同步机制的情况下总是可以读到一个类的静态变量的初始值。final修饰的变量值不会改变。在多线程的环境中,它还会保证两点,1. 其他线程所看到的final字段必然是初始化完毕的。 2. final修饰的变量不会被程序重排序。

二、单例设计模式

2.1 养一只猫

我想养猫,但是只能养一只猫,这里有两个类,一个是猫类和场景类我家:

public class Cat {
    private static final Cat cat = new Cat();

    private Cat(){ }
	
    // 对外提供猫
    public static Cat getInstance () {
        return cat;
    }

    // 猫会叫
    public void say () {
        System.out.println("miaow~");
    }
}
public class Home {
    public static void main(String[] args) {
        // 朋友连续三天都来撸猫了
        for (int i = 0; i < 3; i++) {
            Cat cat = Cat.getInstance();
            cat.say();
        }
    }
}

结果如下

miaow~
miaow~
miaow~

猫很友好,这三天朋友跟它见面都打了招呼,天天见的都是一样的猫。顺手一摸还会喵一声,还是昨天那只猫,感觉老熟了。这就是单例模式。

2.2 单例模式的定义

2.2.1 定义

单例模式(Singleton Pattern)是一个比较简单的模式,定义如下:

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

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

Singleton 类称为单例类,通过使用 private 的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在 Singleton 中自己使用 new Singleton())。

2.2.2 通用类图

单例模式通用类图如下图所示:
单例模式通用类图

2.2.3 特点

单例模式有以下的特点:

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这一实例
2.2.4 通用源代码

单例模式通用源代码:

public class Singleton {
    private static final Singleton singleton = new Singleton();
    
    // 限制产生多个对象
    private Singleton () {    }
    
    // 通过该方法获得对象。
    public static Singleton getInstance() {
        return singleton;
    }
    
    // 类中的其他方法,尽量是static
    public static void doSomething() {    }
}

2.3 单例模式的应用

为什么不使用全局变量确保一个类只有一个实例呢?

因为全局变量分为静态变量和实例变量,静态变量也可以保证该类的实例只存在一个。只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。

但是,如果说这个对象非常消耗资源,而且程序某次的执行中一直没用,这样就造成了资源的浪费。利用单例模式的话,我们就可以实现在需要使用时才创建对象,这样就避免了不必要的资源浪费。 不仅仅是因为这个原因,在程序中我们要尽量避免全局变量的使用,大量使用全局变量给程序的调试、维护等带来困难。

2.3.1 单例模式的优点
  • 由于单例模式在内存中只有一个实例,减少了内存开支。(特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。)
  • 由于单例模式只生成一个实例,所以减少了系统的性能开销。(当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决。在Java EE中采用单例模式时需要注意JVM垃圾回收机制。)
  • 单例模式可以避免对资源的多重占用。(例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。)
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问。(例如可以设计一个单例类,负责所有数据表的映射处理。)
2.3.2 单例模式的缺点
  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
  • 单例模式对测试是不利的。
  • 单例模式与单一职责原则有冲突。(一个类应该只实现一个逻辑,而不关心它是否单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。)
2.3.4 单例模式的使用场景

在一个系统中,要求一个类有且只有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:

  • 要求生成唯一序列号的环境。
  • 在整个项目中需要一个共享访问点或共享数据(例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保线程安全的)。
  • 创建一个对象需要消耗的资源过多(如要访问IO和数据库等资源)。
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。

三、单例设计模式的扩展

3.1 单例模式的常用写法

通常单例模式在Java语言中,有两种构建方式:

  • 饿汉方式。指全局的单例实例在类装载时构建(汉子很饿了,马上找东西吃)
  • 懒汉方式。指全局的单例实例在第一次被使用时构建(汉子很懒,葛优躺等人投喂)

在单例模式定义一节中的通常源代码就是饿汉式的,也推荐使用饿汉方式,因为这是线程安全的。

值得注意的是:不管饿汉式还是懒汉式,由于构造函数是私有的,因此单例类不能被继承

这里介绍懒汉式的基础写法:

public class Singleton {
    private static Singleton singleton = null;

    // 限制产生多个对象
    private Singleton () {    }

    // 通过该方法获得对象。
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }

    // 类中的其他方法,尽量是static
    public static void doSomething() {    }
}

在高并发情况下,应当注意单例模式的线程同步问题。这个懒汉式单例模式在低并发的情况下问题不大,如果系统压力增大,并发量增加时则可能在内存中出现多个实例,破坏了最初的预期。因为对象初始化也是需要时间的,如果第一个线程A执行到了singleton = new Singleton(); 但是还没有获取到对象,第二个线程B获得判断的条件也是真,于是继续运行下去,这样最终线程A和B都获得了一个对象,在内存中就出现了两个对象!

解决线程不安全的方法有很多。可以在 getInstance方法前面添加synchronized 关键字,也可以在 getInstance 方法内增加Synchronized来实现,但都不是最优秀的单例模式(还是推荐饿汉式的)。

懒汉式(检查加锁):

public class Singleton {  
      private static Singleton uniqueInstance;  
      private Singleton (){
      }   
      //没有加入synchronized关键字的版本是线程不安全的
      public static synchronized  Singleton getInstance() {
          //判断当前单例是否已经存在,若存在则返回,不存在则再建立单例
	      if (uniqueInstance == null) {  
	          uniqueInstance = new Singleton();  
	      }  
	      return uniqueInstance;  
      }  
 }

synchronized 关键字是比较重量级的锁。由于同步一个方法会降低100倍或更高的性能, 每次调用获取和释放锁的开销应该是可以避免的:一旦初始化完成,获取和释放锁就显得很不必要。而且当线程数量较多时,用Synchronized加锁,会使大量线程阻塞。为解决性能问题,进行如下优化:

  1. 检查变量是否被初始化(不去获得锁),如果已被初始化立即返回这个变量。
  2. 获取锁
  3. 第二次检查变量是否已经被初始化:如果其他线程曾获取过锁,那么变量已被初始化,返回初始化的变量。
  4. 否则,初始化并返回变量。

这就是双重检查加锁(double-checked locking)

懒汉式(双重检查加锁):

public class Singleton {
    private static Singleton uniqueInstance;
    private Singleton() {
    }
    public static Singleton getInstance() {
       // 检查实例,如果不存在,就进入同步代码块
        if (uniqueInstance == null) {
            // 只有第一次才彻底执行这里的代码
            synchronized(Singleton.class) {
               // 进入同步代码块后,再检查一次,如果仍是null,才创建实例
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

这个算法看起来像是解决性能问题的有效解决方案。然而,这一技术还有许多需要避免的细微问题。例如,考虑下面的事件序列:

  1. 线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。
  2. 由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。
  3. 线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有覆盖B使用的内存(缓存一致性)),程序很可能会崩溃。

在J2SE 1.4或更早的版本中使用双重检查锁有潜在的危险,有时工作正常,有时工作不正常:要区分正确的实现和有小问题的实现是很困难的。这取决于编译器、线程的调度和其他并发系统活动,不正确的实现双重检查锁导致的异常结果可能会间歇性出现,重现异常是十分困难的。

在J2SE 5.0中,这一问题被修正了。 volatile 关键字保证多个线程可以正确处理单件实例:

public class DoubleCheckedLockingSingleton {  
    // java中使用双重检查锁定机制,由于Java编译器和JIT的优化的原因系统无法保证我们期望的执行次序。 
    // 在java5.0修改了内存模型,使用volatile声明的变量可以强制屏蔽编译器和JIT的优化工作  
    private volatile static DoubleCheckedLockingSingleton uniqueInstance;  
    
    private DoubleCheckedLockingSingleton() {  
    }  

    public static DoubleCheckedLockingSingleton getInstance() {  
        DoubleCheckedLockingSingleton localRef = uniqueInstance;
            if (uniqueInstance == null) {  
                synchronized (DoubleCheckedLockingSingleton.class) {  
		            localRef = uniqueInstance;
                    if (localRef == null) {  
                        uniqueInstance = localRef = new DoubleCheckedLockingSingleton();
                    }
                }
            }  
        return localRef ;  
    }  
}

注意上面的变量localRef,“似乎”看上去显得有点多余。但是实际上绝大多数时候uniqueInstance已经被初始化,引入ocalRef可以使得volatile的只被访问一次(利用return localRef代替return helper),这样可以使得这个单例的整体性能提升25%。更多可以参考wiki百科

另外懒汉式还有一种写法,利用静态内部类(嵌套类)的方式实现,被称为占位式或者登记式。

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton (){    }

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

静态内部类的实现是懒加载并且是线程安全的。只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance 。内部类 SingletonHolder 在 Singleton 类加载时加载,解决了懒汉式的性能问题,Singleton 在内部类加载时,getIngestance()方法被调用之前实例化,解决了线程不安全问题。

3.2 破坏单例模式

3.2.1 通过反射破坏单例
public class LazyInnerClassSingletonTest {
    public static void main(String[] args) {
        try {
            Class<?> clazz = Singleton.class;
            //通过反射获取私有构造方法
            Constructor constructor = clazz.getDeclaredConstructor(null);
            //强制访问
            constructor.setAccessible(true);
            //暴力初始化
            Object o1 = constructor.newInstance();
            //创建两个实例
            Object o2 = constructor.newInstance();
            System.out.println("o1:" + o1);
            System.out.println("o2:" + o2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果:创建了两个实例,违反了单例

o1:cn.dxystudy.spring.design.pattern.Singleton@7ba4f24f
o2:cn.dxystudy.spring.design.pattern.Singleton@3b9a45b3

防止通过反射破坏单例:在构造方法中做一些限制,使得多次重复创建时,抛出异常:

private LazyInnerClassSingleton() {
    if (Singleton.class != null) {
        throw new RuntimeException("不允许创建多个实例");
    }
}

重新测试,抛出java.lang.reflect.InvocationTargetException异常,有效防止通过反射破坏单例。

3.2.2 通过反序列化破坏单例

如果 Singleton 单例类,不管是常用的哪种写法,都实现了 Serializable 对象序列化接口,默认情况下每次反序列化总会创建一个新的实例对象,可以通过反序列化破坏单例,创建测试:

public class LazyInnerClassSingletonTest2 {
    //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
    //Exception直接抛出
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //Write Obj to file
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(Singleton.getInstance());
        //Read Obj from file
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        Singleton newInstance = (Singleton) ois.readObject();
        //判断是否是同一个对象
        System.out.println(newInstance == Singleton.getInstance());
    }
}

输出结果:false

说明通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了 Singleton 的单例。

补充知识

什么是Serializable接口?

一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才能被序列化。

什么是序列化?

序列化是将对象状态转换为可保持或传输的格式的过程。与序列化相对的是反序列化,它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据。

为什么要序列化对象?

  • 把对象转换为字节序列的过程称为对象的序列化
  • 把字节序列恢复为对象的过程称为对象的反序列化

什么情况下需要序列化?

当我们需要把对象的状态信息通过网络进行传输,或者需要将对象的状态信息持久化,以便将来使用时都需要把对象进行序列化

为什么还要继承Serializable?

那是存储对象在存储介质中,以便在下次使用的时候,可以很快捷的重建一个副本。

防止序列化破坏单例模式的解决方案:只要在Singleton类中定义readResolve就可以解决该问题。

public class Singleton implements Serializable {
    private static Singleton uniqueInstance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized(Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
    
    // 防止序列化破坏单例模式
    private Object readResolve() {
        return uniqueInstance;
    }
}

重新测试,输出结果:true

说明方案有效防止反序列化破坏单例。

对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的。具体问题来源和解决方案都与之有关,这里不再讨论。有兴趣可以查阅此博文(ken007)

3.3 枚举式单例模式

利用枚举方式写法,这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。

为什么要用枚举呢?

前面介绍的通过反射和反序列化来破坏单例,可以看出

  • 私有构造器并不保险
  • 序列化有问题

枚举单例更简洁,自动支持序列化机制,绝对防止多次实例化 ,同时这种方式也是《Effective Java》以及《Java与模式》的作者推荐的方式。

枚举类单例模式:

public enum Singleton {
	 // 定义一个枚举的元素,它就是 Singleton 的一个实例
    INSTANCE;  

    public void doSomeThing() {  
	     System.out.println("枚举方法实现单例");
    }
}

使用方式:

public class ESTest {
	public static void main(String[] args) {
		Singleton singleton = Singleton.INSTANCE;
		singleton.doSomeThing();//output:枚举方法实现单例
	}
}

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

3.4 有上限的多例模式

假如现在我升职加薪了,买了新房子,可以养更多的猫了,但是最多也只能养两只猫,原来的饿汉式和懒汉式都无法满足,这该如何实现呢?这里可以采用另外一种设计模式。

单例模式一般都是默认只有一个实例,如果要有限个数产生实例的话,那么就要保存“有限个数”这个信息,既然是不止一个的实例,那么原来获取实例的时候就直接返回一个实例的设计要改变。保存有限个数可以采用数组、列表、图等数据结构。将每个实例都缓存到统一容器管理,通过唯一标识获取对应的实例 ,这叫做注册式单例模式。注册式单例模式包括枚举式和容器式。枚举类单例模式是饿汉式单例模式的扩展。

Spring中使用的是容器式,下一节会讲到。

现在试试用枚举类来养两只猫:

创建枚举猫类,我要养两只猫,一只叫“pudding”,一只叫“strong”:

public enum CatEnum {
    // 定义猫的元素,它就是 CatEnum 的实例,这里有两只,老大是布丁,老二只是大壮
    PUDDING("布丁", 1),STRONG("大壮", 2);

    // 枚举类的实例是在该类的第一行显式指定的,但依然可以定义构造器,只是构造器被强制为private权限,因此无法通过调用构造器来显示创建实例
    CatEnum(String name, int number){
        this.name = name;
        this.number = number;
    }

    // 枚举类就像普通类一样可以定义(静态)成员变量、(静态)成员方法
    // 枚举类的成员变量、成员方法的调用方式和普通类一样
    private String name;
    private int number;

    // 猫会叫
    public void say() {
        System.out.println(this.name + ": miaow~");
    }
}

现在朋友来撸猫了,顺便一起吃饭:

public class Home {
    public static void main(String[] args) {
        // 我家有两只猫,记住了
        ArrayList<CatEnum> catList = new ArrayList<>();
        catList.add(CatEnum.PUDDING);
        catList.add(CatEnum.STRONG);

        // 这天朋友来撸猫了,这一天朋友和我一起出去吃饭然后回来,一天下来也有三次回家了
        for (int i = 0; i < 3; i++) {
            // 两只猫都会跑来欢迎我们,就是不知道哪只先跑来
            Random random = new Random();
            int j = random.nextInt(2);

            // 我们回家了
            System.out.println("我们第" + i + 1 + "次回家");
            // 看看哪只猫先跑来呢
            CatEnum catEnum = catList.get(j);
            // 我们蹲下来摸一摸,猫猫很享受~
            catEnum.say();
        }
    }
}

结果:

我们第1次回家
大壮: miaow~
我们第2次回家
大壮: miaow~
我们第3次回家
布丁: miaow~

看来大壮很活跃呀!使劲蹭哈哈哈…

四、Spring 中的单例模式

在Spring中,bean 可以被定义为两种模式:prototype(多例)和singleton(原型)。

  • singleton(单例):只有一个共享的实例存在,所有对这个bean的请求都会返回这个唯一的实例。

  • prototype(原型):对这个bean的每次请求都会创建一个新的bean实例,类似于new。

Spring bean 默认是单例模式。这样做的有点是Spring容器可以管理这些Bean的生命周期,决定什么时候创建出来,什么时候销毁,销毁的时候如何处理等等。如果采用费单例模式(prototype),则Bean初始化后的管理交由J2EE容器,Spring容器不在跟踪管理Bean的生命周期。

那 spring 中是使用以上单例模式的哪种写法呢?

很遗憾,都不是,因为以上都有单例类不能被继承的缺点。为了解决饿汉式和懒汉式单例类不能被继承的缺点,可以使用另外一种特殊化的单例模式:将每个实例都登记到某个地方,使用唯一标识获取实例。它被称为单例注册表。上节说到注册式单例模式有两种:枚举式单例模式、容器式单例模式。spring中依赖控制反转的核心概念,使用的是另外一种单例模式实现方式 —— 容器式单例模式。

先看看简单的容器式写法:

public class ContainerSingleton {
    private ContainerSingleton() {
    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getBean(String className) {
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object o = null;
                try {
                    o = Class.forName(className).newInstance();
                    ioc.put(className, o);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return o;
            } else {
                return ioc.get(className);
            }
        }
    }
}

可继承的单例容器:

import java.util.HashMap;
public class RegSingleton{
    static private HashMap registry = new HashMap();
    // 静态块,在类被加载时自动执行
    static {
        RegSingleton rs = new RegSingleton();
        registry.put(rs.getClass().getName(),rs);
    }
    // 受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点
    protected RegSingleton(){}

    // 静态工厂方法,返回此类的唯一实例
    public static RegSingleton getInstance(String name){
        if(name == null){
            name = "RegSingleton";
        }if(registry.get(name)==null){
            try{
                registry.put(name, Class.forName(name).newInstance());
            } catch (Exception ex){ex.printStackTrace();}
        }
        return  (RegSingleton)registry.get(name);
    }
}

Spring对单例的底层实现源代码,getBean的关键都调用了doGetBean 方法,所以doGetBean 方法是十分关键的,一下列出部分源代码

package org.springframework.beans.factory.support;

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    
	... 
        
    private final Set<String> alreadyCreated = Collections.newSetFromMap(new ConcurrentHashMap<>(256));

    // 多个 getBean 方法都调用了doGetBean 方法
	@Override
	public Object getBean(String name) throws BeansException {
		return doGetBean(name, null, null, false);
	}

	//  省略其他方法...
    
	@SuppressWarnings("unchecked")
	protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
			@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
        // 对传入的Bean name稍做处理,防止传入的Bean name名有非法字符(或则做转码) 
		final String beanName = transformedBeanName(name);
		Object bean;

		// 马上检查手动注册过的单例缓存
		Object sharedInstance = getSingleton(beanName);
        // 如果单例缓存有的话
		if (sharedInstance != null && args == null) {
            ...      
            // 返回合适的缓存Bean实例  
			bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
		}
		// 如果单例缓存没有的话
		else {
			// 如果我们已经创建此 bean 实例:要注意循环依赖的问题
			if (isPrototypeCurrentlyInCreation(beanName)) {
				throw new BeanCurrentlyInCreationException(beanName);
			}

			// 检查此工厂中是否存在 bean 定义。
			BeanFactory parentBeanFactory = getParentBeanFactory();
			if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
				// 没找到-> 检查父类
				String nameToLookup = originalBeanName(name);
				if (parentBeanFactory instanceof AbstractBeanFactory) {
					return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
							nameToLookup, requiredType, args, typeCheckOnly);
				}
				else if (args != null) {
					// 使用显式 arg 委派给父级
					return (T) parentBeanFactory.getBean(nameToLookup, args);
				}
				else if (requiredType != null) {
					// 没有 arg -> 委托给标准 getBean 方法
					return parentBeanFactory.getBean(nameToLookup, requiredType);
				}
				else {
					return (T) parentBeanFactory.getBean(nameToLookup);
				}
			}

			...

			try {
				final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
				checkMergedBeanDefinition(mbd, beanName, args);

				// 保证当前 Bean 所依赖的 Bean 已经初始化。
				String[] dependsOn = mbd.getDependsOn();
				if (dependsOn != null) {
					...
				}

				// 创建 bean 实例,如果是单例,做如下处理  
				if (mbd.isSingleton()) {
					sharedInstance = getSingleton(beanName, () -> {
						try {
							return createBean(beanName, mbd, args);
						}
						catch (BeansException ex) {
							...
						}
					});
					bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				}
                // 如果是非单例,即prototpye,每次都要新创建一个Bean实例
				else if (mbd.isPrototype()) {
					// 如果是原型 -> 创建一个新的实例
					Object prototypeInstance = null;
					try {
						beforePrototypeCreation(beanName);
						prototypeInstance = createBean(beanName, mbd, args);
					}
					finally {
						afterPrototypeCreation(beanName);
					}
					bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
				}
				// 其他类型
				else {
                    ...
				}
			}
			catch (BeansException ex) {
				...
			}
		}

		// 检查所需的类型是否与实际 bean 实例的类型匹配。
		if (requiredType != null && !requiredType.isInstance(bean)) {
			...
		}
        // 返回 Bean
		return (T) bean;
	}
	
}

在DefaultSingletonBeanRegistry类中实现了getSingleton方法,这里 Spring 尝试从缓存中加载单例。单例在 Spring 的同一个容器中只会被创建一次,后续再获取 bean,就直接从缓存中取了。类似多级缓存的设计。

	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		Object singletonObject = this.singletonObjects.get(beanName);
        // 这个bean 正处于 创建阶段
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            // 并发控制
			synchronized (this.singletonObjects) {
                // 单例缓存是否存在
				singletonObject = this.earlySingletonObjects.get(beanName);
                // 是否运行获取 bean factory 创建出的 bean
				if (singletonObject == null && allowEarlyReference) {
                    // 获取缓存中的 ObjectFactory
					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
					if (singletonFactory != null) {
						singletonObject = singletonFactory.getObject();
                        // 将对象缓存到 earlySingletonObject中
						this.earlySingletonObjects.put(beanName, singletonObject);
                        // 从工厂缓冲中移除
						this.singletonFactories.remove(beanName);
					}
				}
			}
		}
		return singletonObject;
	}

	public boolean isSingletonCurrentlyInCreation(String beanName) {
		return this.singletonsCurrentlyInCreation.contains(beanName);
	}

DefaultSingletonBeanRegistry 这个类里面的成员变量:

  • Map<String, Object> singletonObjects 这个很好理解、 key 就是 beanName ,value 就是 bean 实例
  • Map<String, ObjectFactory<?>> singletonFactories key 为 beanName,value 为创建 bean 的工厂
  • Map<String, Object> earlySingletonObjects key 为 beanName ,value 为 bean。但是和 singletonObjects 不同的是,bean 被加入到 earlySingletonObjects 的时候、这个 bean 还是处于一种创建中的状态,目的也很简单、Spring 用来解决某些场景下的循环依赖
  • Set<String> singletonsCurrentlyInCreation 这个 Set 中,当创建一个 bean 之前会将其 对应的 beanName 放置到这个 Set 中

源码分析可以参考知乎CoderLi写的文章:

Spring 获取单例流程(一):读完这篇文章你将会收获到

  • getBean 方法中, Spring 处理别名以及 factoryBeanname
  • Spring 如何从多级缓存中根据 beanName 获取 bean
  • Spring 如何处理用户获取普通 beanfactoryBean

Spring 获取单例流程(二):读完这篇文章你将会收获到

  • Springprototype 类型的 bean 如何做循环依赖检测
  • Springsingleton 类型的 bean 如何做循环依赖检测

Spring 获取单例流程(三):读完这篇文章你将会收获到

  • Spring 何时将 bean 加入到第三级缓存和第一级缓存中
  • Spring 何时回调各种 Aware 接口、BeanPostProcessorInitializingBean

五、总结

我从Spring IoC里获取单例Bean开始学习单例模式,单例模式的定义是确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。常用的写法有饿汉式和懒汉式,需要注意线程安全的问题。Spring根据控制反转的概念,需要另外一种写法,就是容器注册式的写法。尝试阅读Spring源码,Spring源码做了很多事情,此篇难以详尽叙述。原来以为单例模式比较简单,结果发现越来越多的知识点,涉及面也包括数据结构与算法、JVM、设计模式等等内容。从spring中学习设计模式收获良多。

参考资料

[1] 朱小厮. 设计模式:单例模式(Singleton)

[2] [朱小厮. singleton模式四种线程安全的实现

[3] [朱小厮. 如何防止单例模式被JAVA反射攻击

[4] JavaGuide. CSDN. 深入理解单例模式——只有一个实例

[5] 皮肤黝黑的小白. 博客园. Spring中常见的设计模式——单例模式

[6] ken007. 博客园. 序列化对单例模式的破坏

[7] nickcenter. 博客园. spring怎么实现单例模式?

[8] 李子沫. 博客园. 为什么要用枚举实现单例模式(避免反射、序列化问题)

[9] 顽强的小弹壳. 简书. 单例模式4-注册式单例(枚举,容器)

[10] 喝醉的香锅锅. 博客园. enum不能被继承.

[11] 奋斗的哼哼. CSDN. 枚举类的使用方法.

[12] D_戴同学. 博客园. spring的bean单例和单例设计模式.

[13] Vincent. 博客园. 单例(Singleton)模式)及原型

[14] 白花蛇草可乐. 简书. Spring的单例bean与原型bean.

[15] Javadoop. Spring IOC 容器源码分析.

[16] CoderLi. 知乎. Spring 获取单例流程(一)

[17] 《单例与序列化的那些事儿》

[18] zzsuje. 博客园. Spring源码阅读笔记06:bean加载之如何获取单例

[19] spring-projects/spring-framework. Github

[20] 《深入浅出Spring Boot 2.x》杨开振

[21] 《设计模式之禅(第二版)》秦小波

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值