进制人生

菜鸟成长系列-单例模式

2018-02-09  本文已影响0人  glmapper_2018

菜鸟成长系列-概述

菜鸟成长系列-面向对象的四大基础特性

菜鸟成长系列-多态、接口和抽象类

菜鸟成长系列-面向对象的6种设计原则

前面已经将设计模式中的基本内容撸了一下,今天开始正式开始设计模式系列的内容,因为网上也有很多关于设计模式的技术博客,从不同的角度对设计模式都做了很详细的解读;本系列的模式除了基本的概念和模型之外,还会结合java自身使用的和Spring中使用的一些案例来进行学习分析。

水平有限,如果存在不当之处,希望大家多提意见,灰常感谢!

设计模式中总体分为三类:

一、创建型(5):

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

二、结构型(7)

三、行为型(11)

单例模式

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

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

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这个类。


image.png image.png

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

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

Spring中的单例

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

org.springframework.beans.factory.suppor.AbstractBeanFactory

image.png

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

image.png

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

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


image.png

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

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


image.png

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

image.png

这里使用的是饿汉式中的双重检测机制来实现的。

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

上一篇下一篇

猜你喜欢

热点阅读