设计模式:单例(”单身狗” is also a cute Dog…)

2019 Java 开发者跳槽指南.pdf (吐血整理)….>>>

前面已经将设计模式中的基本内容撸了一下,今天开始正式开始设计模式系列的内容,因为网上也有很多关于设计模式的技术博客,从不同的角度对设计模式都做了很详细的解读;本系列的模式除了基本的概念和模型之外,还会结合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,至此单例模式的学习就结束了,下一篇文章将会介绍工厂模式(简单工厂,工厂方法,抽象工厂)。


原文始发于微信公众号(glmapper工作室):设计模式:单例("单身狗" is also a cute Dog...)