定义
大家都知道,一个对象的产生都是通过 new 关键字实现的(当然也存在其它方式,比如反射、复制等),new 的实现又是依托于构造函数的,默认一个类会自动生成一个无参的构造函数在不指定构造函数的情况下。构造函数一般都是 public 权限修饰的,想象一下,如果我们将类的构造函数的访问修饰符改为 private 不就可以禁止外部创建该对象了吗?这个时候外部想要实例化该类怎么办呢?
这时,私有化构造函数的类可以提供相应的 “接口”(一般就是静态方法)来返回自己的唯一实例供外部调用,像这样的确保只生成一个实例的模式被称作单例模式。单例模式一般应用在如下场景:
- 想确保任何情况下都绝对只有一个实例
- 想在程序上表现出” 只存在一个实例 “
概括一下就是:
- 只有一个实例
- 自我实例化
- 提供全局访问点
所谓的提供全局访问点,就是说除了公共访问点之外,不能通过其他访问点访问该实例。假设一个类只能创建一个实例,那么该类就称为单例类。
单例模式代码实现
单例模式的主要角色就是单例类,通常该类包含如下实现:
- 私有化的构造函数
- 私有化的类成员变量
- 公共的类实例的访问方法
其 UML 类图大致如下:
单例模式的实现一般有懒汉式和饿汉式两种,分别列举如下,首先是懒汉式:
public class Singleton {
// 使用类变量来缓存创建过的实例
private static volatile Singleton instance = null; // 保证instance线程同步
private Singleton() {}
// 使用synchronized关键字修饰,确保线程安全
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
测试如下:
public class Main {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true
}
}
懒汉式的特点是,类加载时没有创建实例,而是在调用 getInstance 方法时才去创建单例,所以就会存在线程安全性问题。但是每次访问都有同步问题,消耗资源,影响性能,所以建议使用如下饿汉式。
public class Singleton {
// 使用类变量来缓存创建过的实例
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return instance;
}
}
饿汉式就比较好理解,直接在类创建的同时就生成静态成员变量供外部使用,即预先加载法,所以不存在线程安全性问题。
优缺点
单例模式优缺点总结如下:
- 单例模式一般拓展困难,除了修改代码几乎没有选择;
- 单例模式与单一职责原则冲突。一个类,通常只关心它要实现的业务逻辑,但是单例模式既要关心自己是否单例,又要实现业务逻辑,融合性比较高。
应用场景
前面讲过,单例模式只有一个实例,消耗资源少,具体场景如下:
- 要求生成唯一序列号的环境;
- 网站计数器,一般采用单例模式,否则难以同步;
- 文件系统、打印机、资源管理器等,因为底层资源只能同时被一方操纵,所以这些模块暴露的接口必然是单例的
- Java 中的 dao、service 一般都是单例的,而 action 层一般都是多例。
Spring 如何实现单例模式
Spring 框架是我们经常使用的 Java Web 框架,在 Spring 中,Bean 可以被定义为两种模式:prototype(多例)和 singleton(单例)。
所谓的多例:对该 bean 每次请求时都会获取一个新的 bean 实例,类似于 new 操作。
Spring 的 bean 默认是单例模式。bean 的作用域可以通过 bean 标签的 scope 属性进行设置,一般 scope 有如下几种值:
- singleton(单例):任何时候获取到的 bean 都是同一个实例;
- prototype(多例):任何时候获取到的 bean 都是新的实例;
- request:在 WEB 应用程序中,每一个实例的作用域都为 request 范围;
- session:在 WEB 应用程序中,每一个实例的作用域都为 session 范围;
Spring 的单例模式又分为饿汉模式和懒汉模式,其中饿汉模式是缺省模式,懒汉模式则需要在 bean 的定义处使用 default-lazy-init=“true” 来声明为懒汉模式。
那么 Spring 对单例的底层实现,到底是饿汉式单例还是懒汉式单例呢?其实,都不是,Spring 对单例的实现是通过单例注册表的方式实现的,其源码如下:
public abstract class AbstractBeanFactory implements ConfigurableBeanFactory{
/**
* 充当了Bean实例的缓存,实现方式和单例注册表相同
*/
private final Map singletonCache=new HashMap();
public Object getBean(String name)throws BeansException{
return getBean(name,null,null);
}
...
public Object getBean(String name,Class requiredType,Object[] args)throws BeansException{
//对传入的Bean name稍做处理,防止传入的Bean name名有非法字符(或则做转码)
String beanName=transformedBeanName(name);
Object bean=null;
//手工检测单例注册表
Object sharedInstance=null;
//使用了代码锁定同步块,原理和同步方法相似,但是这种写法效率更高
synchronized(this.singletonCache){
sharedInstance=this.singletonCache.get(beanName);
}
if(sharedInstance!=null){
...
//返回合适的缓存Bean实例
bean=getObjectForSharedInstance(name,sharedInstance);
}else{
...
//取得Bean的定义
RootBeanDefinition mergedBeanDefinition=getMergedBeanDefinition(beanName,false);
...
//根据Bean定义判断,此判断依据通常来自于组件配置文件的单例属性开关
//<bean id="date" class="java.util.Date" scope="singleton"/>
//如果是单例,做如下处理
if(mergedBeanDefinition.isSingleton()){
synchronized(this.singletonCache){
//再次检测单例注册表
sharedInstance=this.singletonCache.get(beanName);
if(sharedInstance==null){
...
try {
//真正创建Bean实例
sharedInstance=createBean(beanName,mergedBeanDefinition,args);
//向单例注册表注册Bean实例
addSingleton(beanName,sharedInstance);
}catch (Exception ex) {
...
}finally{
...
}
}
}
bean=getObjectForSharedInstance(name,sharedInstance);
}
//如果是非单例,即prototpye,每次都要新创建一个Bean实例
//<bean id="date" class="java.util.Date" scope="prototype"/>
else{
bean=createBean(beanName,mergedBeanDefinition,args);
}
}
...
return bean;
}
}
这种使用 Map 对象(登记薄)来维护一组单例类的实例又称为登记式单例,不管是饿汉式还是懒汉式,因为其构造函数都是私有不可继承的,Spring 为实现单例类可继承,就使用了单例注册表(登记薄)形式。
登记薄基本功能是:对于已经登记过的单例,则从工厂直接返回,对于没有登记的,则先登记,而后返回。基本点如下:
- 使用 Map 实现注册表
- 使用 protect 取代原先的 private 的构造方法,确保子类可继承
总结
这节我们学习了单例模式,单例模式在工作中使用的还是比较多的,比如要生成唯一序列号、唯一连接对象等等这些都是要用到单例模式的,然后我们又介绍了下单例模式的优缺点以及简单说了下单例注册表的相关知识点,大家可以好好练习下。