菜鸟成长系列-单例模式

1,479 阅读10分钟

菜鸟成长系列-概述
菜鸟成长系列-面向对象的四大基础特性
菜鸟成长系列-多态、接口和抽象类
菜鸟成长系列-面向对象的6种设计原则

前面已经将设计模式中的基本内容撸了一下,今天开始正式开始设计模式系列的内容,因为网上也有很多关于设计模式的技术博客,从不同的角度对设计模式都做了很详细的解读;本系列的模式除了基本的概念和模型之外,还会结合java自身使用的和Spring中使用的一些案例来进行学习分析。
水平有限,如果存在不当之处,希望大家多提意见,灰常感谢!
设计模式中总体分为三类:
一、创建型(5):

  • 工厂方法[Factory Method]
  • 抽象工厂[Abstract Factory]
  • 原型[Prototype]
  • 建造者[Builder]
  • 单例[Singleton]

还有一个简单工厂[Simple Factory],目前有两种,有的把单例模式作为这5种之一,有的是将简单工厂作为这5种之一。这里不做讨论,原则上两个都是,只是划分规则不同。

二、结构型(7)

  • 适配器[Adapter]
  • 桥接[Bridge]
  • 组合[Composite]
  • 装饰[Decorator]
  • 外观[Facade]
  • 享元[Flyweight]
  • 代理[Proxy]

三、行为型(11)

  • 策略[Strategy]
  • 模板方法[Template method]
  • 职责链[Chain of Responsibility]
  • 迭代器[Iterator]
  • 状态[State]
  • 访问者[Visitor]
  • 命令[Command]
  • 备忘录[Memento]
  • 观察者[Observer]
  • 中介者[Mediator]
  • 解释器[Interpreter]

单例模式

首先它是一种创建型模式,与其他模式区别在于:单例模式确保被创建的类只有一个实例对象,而且自行实例化并向整个系统提供这个实例。一般情况下我们称当前这个类为单例类。

从上面这段话中我们可以了解到,单例模式具备以下三个要点:

  • 某个类只能有一个实例
  • 必须自行创建这个实例[具体的对象创建由类本身负责,其他类不负责当前类的创建]
  • 必须向整个系统提供这个实例[也就是说,当前类需要对外提供一个获取当前实例的一个方法,且该方法不能是私有的]

OK,来看单例模式的几种实现方式。

方式一:饿汉式

package com.glmapper.design.singleton;
/**
 * 单例模式-饿汉式
 * @author glmapper
 * @date 2017年12月17日下午10:30:38
 */
public class EagerSingleton {
	/**
	 * 内部直接提供一个eagerSingletonInstance;
	 * 我们知道,一般情况下,如果一个变量被static final修饰了,那么该变量将会被视为常量。
	 * 满足要点:自行创建
	 */
	private static final EagerSingleton eagerSingletonInstance = new EagerSingleton();
	/**
	 * 提供一个私有的构造函数,这样其他类就无法通过new
	 * EagerSingleton()来获取对象了,同样也保证了当前类不可以被继承
	 * 满足要点:某个类只能有一个实例
	 */
	private EagerSingleton(){}
	/**
	 * 对外提供一个获取实例的方法
	 * 满足要点:向整个系统提供这个实例
	 */
	public static EagerSingleton getInstance(){
		return eagerSingletonInstance;
	}
}

方式二:懒汉式

package com.glmapper.design.singleton;
/**
 * 单例模式-懒汉式
 * @author glmapper
 * @date 2017年12月17日下午10:45:54
 */
public class LazySingleton {
	//提供一个私有静态变量,注意区别与饿汉式中的static final。
	private static LazySingleton lazySingletonInstance = null ;
	//同样需要提供一个私有的构造方法,其作用与饿汉式中的作用一样
	private LazySingleton(){}
	/**
	 * 1.使用synchronized来保证线程同步
	 * 2.实例的具体创建被延迟到第一次调用getInstance方法时来进行
	 * 3.如果当前实例已经存在,不再重复创建
	 */
	public synchronized static LazySingleton getInstance(){
		if (lazySingletonInstance == null) {
			lazySingletonInstance = new LazySingleton();
		}
		return lazySingletonInstance;
	}
}

饿汉式单例类在自己被加载时就自己实例化了,即便加载器是静态的,在饿汉式单例类被加载时仍会将自己实例化。从资源利用角度来说,这个比懒汉式单例类稍微的差一些。如果从速度和响应时间来看,饿汉式就会比懒汉式好一些。懒汉式在单例类进行实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题。

方式三:登记式

package com.glmapper.design.singleton;
import java.util.HashMap;
/**
 * 单例模式-登记式
 * @author glmapper
 * @date 2017年12月17日下午10:58:36
 */
public class RegisterSingleton {
	//提供一个私有的HashMap类型的registerSingletonInstance存储该RegisterSingleton类型的单例
	private static HashMap<String,Object> registerSingletonInstance = new HashMap<>();
	//通过static静态代码块来进行初始化RegisterSingleton当前类的实例,并将当前实例存入registerSingletonInstance
	static {
		RegisterSingleton singleton = new RegisterSingleton();
		registerSingletonInstance.put(singleton.getClass().getName(), singleton);
	}
	/**
	 * 注意区别,此处提供的是非private类型的,说明当前类可以被继承
	 */
	protected RegisterSingleton(){}
	/**
	 * 获取实例的方法
	 */
	public static RegisterSingleton getInstance(String name){
		//如果name为空,则那么默认为当前类的全限定名
		if (name == null) {
			name ="com.glmapper.design.singleton.RegisterSingleton";
		}
		//如果map中没有查询到指定的单例,则将通过Class.forName(name)来创建一个实例对象,并存入map中
		if (registerSingletonInstance.get(name)==null) {
			try {
				registerSingletonInstance.put(name, Class.forName(name).newInstance());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		//返回实例
		return (RegisterSingleton) registerSingletonInstance.get(name);
	}
}

登记式单例是Gof为了克服饿汉式和懒汉式单例类均不可被继承的缺点而设计的。

package com.glmapper.design.singleton;
/**
 * 登记式-单例-子类
 * @author glmapper
 * @date 2017年12月17日下午11:14:03
 *
 */
public class ChildRegisterSingleton extends RegisterSingleton
{
	/**
	 * 由于子类必须允许父类以构造方法调用产生实例,因此,子类的构造方法必须
	 * 是public类型的。但是这样一来,就等于说可以允许以new 
	 * ChildRegisterSingleton()的方式产生实例,而不必在父类的登记中。
	 */
	public ChildRegisterSingleton(){}	
	
	//客户端测试获取实例
	public static void main(String[] args) {
		ChildRegisterSingleton crs1 = (ChildRegisterSingleton) getInstance(
				"com.glmapper.design.singleton.ChildRegisterSingleton");
		ChildRegisterSingleton crs2 = (ChildRegisterSingleton) getInstance(
				"com.glmapper.design.singleton.ChildRegisterSingleton");
		System.out.println(crs1 == crs2);
	}
}

返回:true   这个同志们可以自行验证,肯定是一样的。但是不能使用new,
因为前提约束是,需在父类中登记的才是单例。

方式四:双重检测模式,双重检测方式在某些书上或者文献中说对于java语言来说是不成立的,但是目前确实是通过某种技巧完成了在java中使用双重检测机制的单例模式的实现,;这种技巧后面来说;关于为什么java语言对于双重检测成例不成立,大家可以在[BLOCH01]文献中看下具体情况。
先来看一个单线程模式下的情况:

package com.glmapper.design.singleton;
/**
 * 一个错误的单例例子
 * @author glmapper
 * @date 2017年12月17日下午11:53:04
 */
public class DoubleCheckSingleton {
	private static DoubleCheckSingleton instance=null;
	public static DoubleCheckSingleton getDoubleCheckSingleton(){
		if (instance == null) {
			instance = new DoubleCheckSingleton();
		}
		return instance;
	}
}

这个很明显是一个错误的例子,对于A/B两个线程,因为step 1并没有使用同步策略,因此线程A/B可能会同时进行// step 2,这样的话,就会可能创建两个对象。那么正确的方式如下:使用synchronized关键字来保证同步。

package com.glmapper.design.singleton;
/**
 * 这是一个正确的打开方式哦。。。
 * @author glmapper
 * @date 2017年12月17日下午11:53:04
 */
public class DoubleCheckSingleton {
	private static DoubleCheckSingleton instance=null;
	//使用synchronized来保证getDoubleCheckSingleton同一时刻只能被一个线程访问
	public synchronized static DoubleCheckSingleton getDoubleCheckSingleton(){
		if (instance == null) {
			instance = new DoubleCheckSingleton();
		}
		return instance;
	}
}

这种方式虽然保证了线程安全性,但是也存在另外一种问题:同步化操作仅仅在instance首次初始化操作之前会起到作用,如果instance已经完成了初始化,对于getDoubleCheckSingleton每一次调用来说都会阻塞其他线程,造成一个不必要的瓶颈。那我们就通过使用更加细粒度化的锁,来适当的减小额外的开销。OK,下面再来一个错误的例子:

package com.glmapper.design.singleton;
/**
 * 一个错误的单例例子
 * @author glmapper
 * @date 2017年12月17日下午11:53:04
 */
public class DoubleCheckSingleton {
	private static DoubleCheckSingleton instance=null;
	//使用synchronized来保证getDoubleCheckSingleton同一时刻只能被一个线程访问
	public static DoubleCheckSingleton getDoubleCheckSingleton(){
		if (instance == null) {  //1
		    // B线程检测到uniqueInstance不为空
			synchronized (DoubleCheckSingleton.class) { //2
				if (instance == null) { //3
					instance = new DoubleCheckSingleton();//4
					// A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。
				}
			}
		}
		// 后面B线程执行时将引发:对象尚未初始化错误。
		return instance;//5
	}
}

看起来没什么毛病呀?我们来分析,两个线程A和B,同时到达1,且都通过了1的检测。此时A到了4,B在2。此时B线程检测到instance不为空,A线程被指令重排了,刚好先赋值了;但还没执行完构造函数;再接下来B线程执行时将引发:对象尚未初始化错误(5)。

对于上面的问题,我们可以通过volatile关键字来修饰instance对象,来保证instance对象的内存可见性和防止指令重排序。这个也就是前面说到的“技巧”。

private static DoubleCheckSingleton instance=null;
改为:
private static volatile DoubleCheckSingleton instance=null;

本篇将单例模式的几种情况进行了分析。后面将会对将java中和Spring中所使用的单例场景进行具体的案例分析。

JAVA中的单例模式使用

JAVA中对于单例模式的使用最经典的就是RunTime这个类。

注释解读:每个Java应用程序都有一个Runtime类的单个实例,允许应用程序与运行应用程序的环境进行交互。 当前运行时可以从getRuntime方法获得。应用程序不能创建它自己的这个类的实例。

看过上篇文章的小伙伴可能比较清楚,这里RunTime使用的是懒汉式单例的方式来创建的。Runtime提供了一个静态工厂方法getRuntime方法用于获取Runtime实例。Runtime这个类的具体源码分析和只能此处不做分析。

Spring中的单例

Spring依赖注入Bean实例默认是单例的。Spring中bean的依赖注入都是依赖AbstractBeanFactory的getBean方法来完成的。那我们就来看看在getBean中都发生了什么。

org.springframework.beans.factory.suppor.AbstractBeanFactory

从上面这张图中我们啥也看不出,只知道在getBean中又调用了doGetBean方法(Spring中还有java源码中有很多类似的写法,好处在于我们可以通过子类继承,继而编写我们自己的处理逻辑)。OK,再来看看doGetBean方法。

来看下这个方法的注释:返回指定的bean可以共享或独立的实例 (谷歌+有道+百度)

  • name:要检索的bean的名称
  • requiredType:要检索的bean所需的类型
  • args:如果使用静态工厂方法的显式参数创建原型,则使用参数。 在其他情况下使用非空args值是无效的。
  • typeCheckOnly:获得实例是否是为了类型检查,而不是实际的使用

这个方法体内的代码非常的多,那么我们本文不是来学习Spring的,所以我们只看我们关心的部分,

为手工注册的singleton检查单例缓存。,从这个注释可以看出,此处就是我们获取实例的地方,再往下看。

此处和上面的getBean一样,也是通过模板方法的方式进行调用的。

OK,这里我们看到了获取单例实例的具体实现过程。 返回注册在给定名称下的(原始的)singleton对象。检查已经实例化的单例,并且还允许提前引用当前创建的单例(解析循环引用)。
这里使用的是饿汉式中的双重检测机制来实现的。

OK,至此单例模式的学习就结束了,下一篇文章将会介绍工厂模式(简单工厂,工厂方法,抽象工厂)。