设计模式(4)——单例模式的学习及其六大战将

单例模式的引发的陈年回忆

  记着N年以前,那时候还在上大学,有一门科目叫做软件体系结构,教我们的老师是个40岁左右的女老师,姓韩,好像是东北大学博士毕业的,之所以对她还有些印象,那是因为初恋女友Y同学是被保送到东北大学读研的,而当时韩老师和Y同学的师生关系好像还挺不错,当然也是Y同学确定保送东北大学读研的那一年,我们和平而坚决地分开了。。

  我就读的大学是一所普通本科,我们学院一个年级可以保送的名额也仅仅只有三个,比起985 211那成堆的保送名额真的差的太远,Y同学是相当优秀的,大学四年,每次期末成绩必然是专业第一,而我,虽然一直在角落里面默默努力着,可成绩总是不尽人意,中游水平,也是因为某次关键的考试少考了一名,错过了成为党员的机会。。。。

  后来我也加入了考研大军,虽然最终成绩比国家线高了50多分,可是距离我的目标院校还少了10分左右,所以我又输了。。。。

  于是在调剂,二战,工作之间,,纠结半天,选择了我最无奈但是方向最明确的一个:工作。毕竟,我父母年纪不小了,是时候赚钱了。。

  我大学时技术不好,或者说大部分人普遍技术都不好,虽然是软件工程专业的,但是实践机会太少,大部分时间都在上那些无聊的理论课,有些理论课,可以无聊到让全班大部分人睡着或者上课玩手机,比如离散数学,计算机组成原理。。 

  而软件体系结构这门课,算是无聊中的理论课相对有点激情的科目,因为,韩老师每次上课前都提问上节课的知识点。。。还会慷慨激昂的挥斥方遒指点江山,鼓励我们考研考研!!这门课就是给我们讲解设计模式的,不过,那些设计模式具体实现我大多忘却了,只记着一些名字:工厂模式,抽象工厂模式,桥接模式,适配器模式,命令模式,建造者模式,观察者模式,迭代器模式,策略模式....有一个很有趣的模式我却还记着比较清楚:单例模式。因为当时分为懒汉式和饿汉式,自己感觉很有趣(那时的快乐就那么简单),而且找工作时也有面试官让我手写单例模式(幸亏写出来了)。。 

  最近随意点开了一篇博客,讲单例模式的,原本没想仔细看,然而却发现,单例模式居然有六种实现模式,而我知道的两种仅仅是最简单的两种,突然感觉自己很无知。。(啪啪打脸)

 于是乎,自己拜读一番,然后写了点demo。。今天也是打算正儿八经聊聊单例模式的。。不过有些触景生情,一不小心扯远了。

 好吧,现在我们言归正传,开始聊单例模式和它的六种实现。

 再稍等下,我想说,当年软件体系结构这门课也是考了89的人,不高但是也不低啊,嘻嘻。

单例模式--从私有化无参构造函数开始

  我不知道该怎么陈述我的话语,以前写博客都是大摆理论知识加点自写的demo代码,坦白讲,连我自己都不想看,于是想从这篇开始做出一些改变,用通俗的话语来讲技术理论讲明白,第一次尝试这种风格,还请观众给点勇气,多多鼓掌。

  所谓单例模式,从字面上来讲,那就是单个实例的模式,只创造一个实例。我们想一下哈,平时工作写代码时,是不是动不动就new一下,动不动就给一个实体创造一个对象,那样的话我们就可以针对某个实体创造多个了实例了,我们都明白,在新建一个实体后,即使不声明构造方法java也是会默认的提供一个无参构造方法的,而我们平时new 实例也就是通过这个无参构造方法实现的,现在将这个无参构造方法声明为私有的,那么就不允许外部去创建了,举个例子。

Zae z1 = new Zae();
Zae z2 = new Zae();
Zae z3 = new Zae();

  

  这种未声明私有时,可以创建了3个实例z1,z2,z3哎,但是你一旦在实体中加入:private Zae(){}。 那么你再这样写,保准红色波浪线等着你,编译期就给你亮红灯。


  那么我们又有疑问了,单例模式既然是不允许创造多个实例,但是允许创造一个实例啊,你这样不就一个实例也创造不出来么,你该怎么样挡住悠悠之口。


  接下来,将是小z给你讲一下实现单例的方式了,在讲之前,先给大家看一段最简单的单例模式实现代码:

public class SingletonBeanHunger {

private SingletonBeanHunger(){}

private static SingletonBeanHunger singletonBeanHunger = new SingletonBeanHunger();

public static SingletonBeanHunger getInstance(){
    return singletonBeanHunger;
}

public void draw(){
    System.out.println("hunger-singleton");
}
}

  

  有没有看到,有没有看到,无参构造函数率先被声明私有的了,而创建的那唯一各一个单例也是被声明私有的了,只是提供了一个公有的调用返回方法。可能你此刻有点蒙,小Z你刚刚说java会默认提供一个无参构造方法,但是你现在声明的这个私有的无参构造方法明明是你自己写的,和系统提供的有什么关系?哈哈,那是因为java的一种默认机制了,当没有定义构造方法时每个类里都有一个默认的无参的构造方法,此时该类就只有一个构造方法;而当你显示的去定义类的构造方法时,那就没有那个默认的构造方法了,该类所有的构造方法就是定义了的那些构造方法。  

  有心人发现,为什么要将创建单例的代码变量声明为static静态的方法,这就是高明之处,也是重点了,因为类的加载机制,大家都明白变量里面有静态变量和成员变量的,而静态变量它的大哥是类(Class),类创建它就创建,类销毁它就销毁,而成员变量的大哥是对象,也就是那个所谓的实例,对象创建出来它就创建,对象销毁它也销毁。因此呢,类指定只有一个的,也就是只会被加载一次,那么由此引发的静态变量也就创建出这么独一份了,进而起到了单例的效果。而提供公共的调用方法getInstance(),这个方法时将类加载时创建的那个单例返回出去,因此别人想使用这个单例时,拿到的也就是独一份的了,地址也是相同的。至于那个微不足道的draw()方法,这不是重点,因为它是个成员方法,也就只有对象可以调用了,这个是用来测试我调用getInstance()时拿到的是SingletonBeanHunger这个类的对象而已。


  通过上述的讲解大家也差不多的明白单例的概念以及简单实现逻辑,记住这几点:1、单例只能有一个实例。2、单例类必须自己创建自己的唯一实例。3、单例类必须给所有其他对象提供这一实例。其实单例模式吧它主要是解决一个全局使用的类频繁的创建和销毁的,毕竟有时太过频繁创建销毁实例,对JVM也是一个极大的挑战,所以当我们想控制实例的数目和节省系统资源时,不妨考虑下单例模式。他虽然优点不少,但是也有些不可避免的缺点,无参构造都被私有化了,那么当然不能被继承了,也就没有啥接口了,否则就和单一职责原则冲突了。


单例模式和它的六大战将

  其实大家看到这个标题还是有点蒙的,六大战将是什么鬼,你怎么不说是四大天王呢,你咋不上天呢。(汗。。)其实六大战将就是六种实现方式了,因为最近在看部网络水文,其中就讲到欧洲第一杀手“皇帝”和他的八大战将,所以就想到了六大战将了,哈哈哈,我真乃人才也。。(还要不要脸。。)

战将一:伯爵(懒汉式-线程不安全)

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading(延迟加载) 很明显,不要求线程安全,在多线程不能正常工作。

/**
 * 懒汉式加载-线程不安全
 */
public class SingletonBeanLazyDg {

    private SingletonBeanLazyDg(){}

    private static SingletonBeanLazyDg singletonBeanLazyDg;

    public static  SingletonBeanLazyDg getInstance(){
     if(singletonBeanLazyDg == null){
       singletonBeanLazyDg = new SingletonBeanLazyDg();
     }
    return singletonBeanLazyDg;
 } 

public void draw(){
System.out.println(
"lazy-singleton-dangerous");
}
}

 

战将二:耶稣(懒汉式-线程安全)

这种方式具备很好的 lazy loading(延迟加载),能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

/**
 * 懒汉式加载-线程安全
 */
public class SingletonBeanLazy {

    private SingletonBeanLazy (){}

    private static SingletonBeanLazy singletonBeanLazy;

    public static synchronized SingletonBeanLazy getInstance(){
        if(singletonBeanLazy==null){
            singletonBeanLazy = new SingletonBeanLazy();
        }
        return singletonBeanLazy;
    }

    public void draw(){
        System.out.println("lazy-singleton");
    }
}

 

战将三:野兽(饿汉模式)

这种方式没有加锁,执行效率会提高。但是类加载时就初始化,容易产生垃圾对象,浪费内存。 它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading (延迟加载)的效果。

/**
 * 饿汉式
 */
public class SingletonBeanHunger {

    private SingletonBeanHunger(){}

    private static SingletonBeanHunger singletonBeanHunger = new SingletonBeanHunger();

    public static SingletonBeanHunger getInstance(){
        return singletonBeanHunger;
    }

    public void draw(){
        System.out.println("hunger-singleton");
    }
}

 

战将四:鬼影(双检锁/双重校验锁(DCL,即 double-checked locking))

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

/**
 * 双重校验锁/双检锁
 */
public class SingletonBeanDCL {

    private SingletonBeanDCL(){}

    private static volatile SingletonBeanDCL singletonBeanDCL;

    public static SingletonBeanDCL getInstance(){
        if(singletonBeanDCL==null){
            synchronized (SingletonBeanDCL.class){
                if (singletonBeanDCL == null){
                    singletonBeanDCL = new SingletonBeanDCL();
                }
            }
        }
        return singletonBeanDCL;
    }

    public void draw(){
        System.out.println("DCL-singleton");
    }
}

 

战将五:金童(登记式/静态内部类)

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

/**
 * 登记式/静态内部类
 */
public class SingletonBeanStatic {

    private SingletonBeanStatic(){}

    private static class  SingletonBeanHolder{
        private static final SingletonBeanStatic SINGLETON_BEAN_STATIC = new SingletonBeanStatic();
    }

    public static final SingletonBeanStatic getInstance(){
        return SingletonBeanHolder.SINGLETON_BEAN_STATIC;
    }

    public void draw(){
        System.out.println("登记式/静态内部类");
    }
    
}

 

战将六:玉女(枚举)

虽然没被广泛应用,但是这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

/**
 * 枚举
 */
public enum SingletonBeanEnum {
    INSTANCE;
    public void draw(){
        System.out.println("枚举式单例");
    }
}

 

大Boss:皇帝(Main-单例)

我只是一个无辜的测试类

/**
 * 测试单例六种实现
 */
public class TestSingle {
    public static void main(String[] args) {
        //懒汉式-线程不安全
        SingletonBeanLazyDg singletonBeanLazyDg = SingletonBeanLazyDg.getInstance();
        singletonBeanLazyDg.draw();
        
        //饿汉式-线程安全
        SingletonBeanHunger singletonBeanHunger = SingletonBeanHunger.getInstance();
        singletonBeanHunger.draw();

        //懒汉式
        SingletonBeanLazy singletonBeanLazy = SingletonBeanLazy.getInstance();
        singletonBeanLazy.draw();

        //双检锁/双重锁校验
        SingletonBeanDCL singletonBeanDCL = SingletonBeanDCL.getInstance();
        singletonBeanDCL.draw();

        //登记式/静态内部类
        SingletonBeanStatic singletonBeanStatic = SingletonBeanStatic.getInstance();
        singletonBeanStatic.draw();

        //枚举
        SingletonBeanEnum singletonBeanEnum = SingletonBeanEnum.INSTANCE;
        singletonBeanEnum.draw();
    }
}

 

獐死于麝 鹿死于角
危险和荣誉总是成正比哒
请大家多多批评指教哈

posted @ 2020-09-27 14:47  帝莘  阅读(534)  评论(4编辑  收藏  举报