这怕是最全的【单例模式】,可以拉着面试官掰扯半小时(面试必备)

例模式是面向对象的编程语言23种设计模式之一,属于创建型设计模式。主要用于解决对象的频繁创建与销毁问题,因为单例模式保证一个类仅会有一个实例。大部分对单例模式应该都知道一些,但面试的时候可能回答不会很完整,不能给自己加分,甚至扣分。

单一的知识点并不能对自己在面试的时候带来加分,而系统的知识树则会让面试官另眼相看,而本文会系统的介绍单例模式的基础版本与完美版本,基本上将单例模式的内容完全包括。如果认为有不同的意见可以留言交流。

单例模式最重要的就是保证一个类只会出现一个实例,那么超过一个就不能被称为是单例,所有其代码构成如下特点。

私有化构造器,禁止从外部创建单例对象。

提供一个全局的访问点获取单例对象。

什么是全局访问点? 好吧,上面的话语太文邹邹了,如果我说公共的静态方法呢?

饿汉、懒汉

主要分为饿汉模式和懒汉模式。那何为饿汉?何为懒汉?

小丽的爸爸从小生活很艰苦,经历了饥荒年代,所以对食物非常紧张。当小丽去上学的时候,不管小丽是否需要,都会给小丽准备很多的零食。

而小明的爸爸则是一个非常懒惰的人,所有的事情都会到最后才去做,所有事情只有当有别人来叫他的时候,他才会把事情做完这样就引出了我们对饿汉模式和懒汉模式的定义:

饿汉模式:不管单例对象是否被使用,都会先创建出一个对象。饿汉模式存在资源浪费的问题,因为很有可能对象创建出来只会永远都不会被使用到。

代码如下:

package demo.single;

/**

* 饿汉模式

*/

publicclassHungrySingle{

/**

    * 饿汉模式,不管hungrySingle对象是否有使用到,都会先创建出来

    * 由于饿汉模式在对象使用之前就已经被创建,所以是不会存在线程安全问题

    */

privatestaticHungrySingle hungrySingle =newHungrySingle();

/**

    * 私有化构造器,禁止外部创建

    */

privateHungrySingle(){

    }

/**

    * 提供获取实例的方法

    */

publicstaticHungrySinglegetInstance(){

returnhungrySingle;

    }

}

懒汉模式:不会先将对象创建出来,而是等到有人使用的时候才会创建。相比饿汉模式,懒汉模式不会存在资源浪费的情况,所以基本都会选择懒汉模式。

代码如下:

package demo.single;

/**

* 懒汉模式

*/

publicclassLazySingle{

/**

    * 懒汉模式,不会先创建对象,而是在调用的时候才会创建对象

    */

privatestaticLazySingle lazySingle =null;

privateLazySingle(){

    }

/**

    * 调用的时候创建对象并返回

    */

publicstaticLazySinglegetInstance(){

if(lazySingle ==null){

lazySingle =newLazySingle();

        }

returnlazySingle;

    }

}

小李:面试官,您看我这样的解释可还行。

面试官:单线程下是挺好的,如果在多线程环境下呢?

小李:这个我知道,加锁啊!

面试官:出门左转电梯直达!

其实加锁也没答错,关键问题在于如何加锁!

直接将获取实例的方法内容写入同步代码块中,解决了多线程安全的问题,但是并发效率的问题又暴露了出来。你想啊,现在锁住了这方法,而无论单例的对象是否创建,都会经过获取锁、释放锁的过程。这样的性能显然是不能接受的。

小李:我想想啊~~~! Emmmmm...! 有了,我们可以在同步代码块外层加一个判断,如果对象已经创建则直接返回。

面试官:这样解决了一部分的并发效率问题,但是如果在创建的时候同时有很多的线程访问,是不是也会有并发的效率问题呢?再优化优化。

小李一想,确实是这样,如果对象还没有创建出来的时候,就有很多的线程来访问,也会出现问题,假设有两个线程同时访问,当A线程优先争抢到锁,A进入同步代码块执行,此时B没有争抢到锁,将处于等待状态,而当A线程执行完成后释放锁,B进入同步代码块执行,此时B线程同样会创建出一个对象,破坏了单例。

小李:面试官,我明白了,可以在同步代码块中再加一层if判断,如果对象已经创建,就直接返回即可。

Double Check

上面最后的结果就是我们常说的Double Check,即双重锁检查。双重锁检查在很多地方都被运用到,代码如下。

packagedemo.single;

/**

* 懒汉模式

*/

publicclassLazySingle{

/**

    * 懒汉模式,不会先创建对象,而是在调用的时候才会创建对象

    */

privatestaticLazySingle lazySingle =null;

privateLazySingle() {

    }

/**

    * 调用的时候创建对象并返回

    */

publicstaticLazySingle getInstance(){

//first check

if(lazySingle ==null){

synchronized(LazySingle.class){

//doublecheck

if(lazySingle ==null){

lazySingle =newLazySingle();

                }

            }

        }

returnlazySingle;

    }

}

面试官:小李,你多线程运行一下代码看看呢。

小李:好勒! 好像挺正常啊。等等, 好像不对, 这里还是出现了多个对象!!!啊~~,这是为什么啊,我都懵了,这完全超出了我的能力范围。

面试官:哈哈,小子,这下知道谁是大佬了吧?我来给你好好解释一下,其实,这和我们的代码没有关系,正常来讲,应该不会出现这样的问题,但是我们都知道,代码在运行过程中,会被编译成一条一条的指令运行,而JVM在运行时,在保证单线程最终结果不会受影响的情况下,对指令进行优化,就有可能对指令进行重排序,同样会破坏单例。

lazySingle=newLazySingle();

//这样一段代码在运行时会生成3条指令,即:1\.分配内存空间2\.创建对象3\.指向引用

//正常情况下是会按照123顺序执行,但JVM优化器进行指令重排后,则可能变为:1\.分配内存空间3\.指向引用2\.创建对象

//在单线程下,这样的优化没有问题,但是多线程下,线程是在争抢CPU时间碎片的。假设A刚刚执行完13//条指令,此时B争抢到时间碎片,发现对象不为空了,就直接返回,但此时对象还没有真正被创建。B调用

//此对象就会抛出异常

//而volatile关键字修饰的变量可以禁止指令重排序,则可以保证指令会是123顺序执行。

//加上volatile修饰

privatevolatilestaticLazySinglelazySingle=null;

小李: 终于解决了,好难啊,一个简单的单例模式居然有这么多的细节。

面试官:你以为这就完了?

内部类的单例

使用内部类的方式可以非常完美的完成单例模式,而实现代码也非常简单。

package demo.single;

/**

* 内部类的方式实现单例

*/

publicclassInnerSingle{

/**

    * 私有化构造器

    */

privateInnerSingle(){

    }

/**

    * 私有内部类

    */

privatestaticclassInner{

//Jingtai内部类持有外部类的对象

publicstaticfinalInnerSingle SINGLE =newInnerSingle();

    }

/**

    * 返回静态内部类持有的对象

    */

publicstaticInnerSinglegetInstance(){

returnInner.SINGLE;

    }

}

可以看到,代码中并没有出现同步方法或者同步代码块,那么静态内部类的方式是如何做到安全的单例模式呢?

外部类加载的时候,不会立即加载内部类,而是在调用的时候会加载内部类。

不管多少线程访问,JVM一定会保证类被正确的初始化,即静态内部类的方式是在JVM层面保证了线程安全

当然,这样也有一些缺点,那就是在创建单例对象的时候,如果需要传参,那么静态内部类的方式会非常麻烦。

破坏单例

那么,上面的单例已经完美了吗?并没有,看我如何将单例给破坏掉。

反射破坏

反射可以绕过私有构造器的限制,创建对象。当然正常的调用是不会发生单例被破坏的情况,但是如果偏偏有人不走寻常路呢,比如下面的调用。

packagedemo.single;

importjava.lang.reflect.Constructor;

/**

* 反射破坏单例

*/

publicclassRefBreakSingleTest{

publicstatic void main(String[] args) throws Exception {

//获取类对象

Class lazySingleClass = LazySingle.class;

        //获取构造器

Constructorconstructor= lazySingleClass.getDeclaredConstructor(null);

constructor.setAccessible(true);

        //创建对象

LazySingle lazySingle =constructor.newInstance(null);

System.out.println(lazySingle);

System.out.println(LazySingle.getInstance());

System.out.println(lazySingle == LazySingle.getInstance());

    }

}

image

<figcaption>测试结果</figcaption>

很明显看到出现了两个不同的兑现,显然,单例被破坏了! 对于这样的情况该如何禁止呢?在网上查阅了很多资料,大部分是使用变量控制法,即在类中添加一个变量用于判断单例类的构造器是否有被调用,代码如下。

//添加变量控制,防止反射破坏

privatestaticbooleanisInstance =false;

privatevolatilestaticLazySingle lazySingle =null;

privateLazySingle()throwsException{

if(isInstance){

thrownewException("the Constructor has be used");

      }

isInstance =true;

  }

再次调用测试代码,发现不能再创建多个单例对象,程序抛出了异常。

image

<figcaption></figcaption>

但是别忘了,属性也是可以通过反射修改的(count、instance的判断反射都能绕过)。

publicclassRefBreakSingleTest{

publicstatic void main(String[] args) throws Exception {

//获取类对象

Class lazySingleClass = LazySingle.class;

        //获取构造器

Constructorconstructor= lazySingleClass.getDeclaredConstructor(null);

constructor.setAccessible(true);

        //创建对象

LazySingle lazySingle =constructor.newInstance(null);

System.out.println(lazySingle);

Field isInstance = lazySingleClass.getDeclaredField("isInstance");

isInstance.setAccessible(true);

isInstance.set(null,false);

System.out.println(LazySingle.getInstance());

System.out.println(lazySingle == LazySingle.getInstance());

    }

}

image

<figcaption></figcaption>

单例再次被破坏,感觉是不是已经快崩溃了,一个单例咋这么多事呢!!既然私有属性、私有方法在外部都能通过反射获取,那有没有反射不能获取的呢?我在网上也找到了另外一种写法,即私有内部类的来持有实例控制变量,而我也通过测试,发现反射同样能够绕过从而破坏单例。

package demo.pattren.single;

importjava.lang.reflect.Constructor;

importjava.lang.reflect.Method;

publicclassBreakInnerTest {

publicstaticvoidmain(String[] args) throws Exception {

        Class<LazySingle> lazySingleClass = LazySingle.class;

//        //获取构造器

Constructorconstructor= lazySingleClass.getDeclaredConstructor(null);

constructor.setAccessible(true);

//创建对象

LazySingle lazySingle = constructor.newInstance(null);

//获取内部类的类对象

Class aClass = Class.forName("demo.pattren.single.LazySingle$InnerClass");

        Method[] methods = aClass.getMethods();

        Constructor<?>[] declaredConstructors = aClass.getDeclaredConstructors();

        System.out.println(declaredConstructors);

Constructor declaredConstructor = declaredConstructors[0];

declaredConstructor.setAccessible(true);

//创建内部类需要传入一个外部类的对象

Objecto = declaredConstructor.newInstance(lazySingle);

//成功绕过

methods[0].invoke(o);

    }

}

目前网上基本都是这两种,但是反射都是能够绕过判断进行破坏。可以这样认为,这种方式反射是可以破坏的,不能100%保证单例不被破坏。欢迎各位提供完美的示例。

序列化破坏

Java的IO提供了对象流,用来将对象写入磁盘、从磁盘读取对象的功能。这也成为了单例的破坏点。

publicstaticvoidmain(String[] args) throws Exception{

//正常的方式获取单例对象

        InnerSingle instance = InnerSingle.getInstance();

//写入磁盘

FileOutputStream fos =newFileOutputStream("d:/single");

ObjectOutputStream oos =newObjectOutputStream(fos);

        oos.writeObject(instance);

        oos.close();

        fos.close();

//从磁盘读取对象

FileInputStream fis =newFileInputStream("d:/single");

ObjectInputStream ois =newObjectInputStream(fis);

        InnerSingle innerSingle = (InnerSingle) ois.readObject();

System.out.println(instance);

System.out.println(innerSingle);

System.out.println(innerSingle == instance);

    }

image

<figcaption></figcaption>

而序列化的方式JVM提供了一种机制,可以防止单例被破坏,即在单例类中添加readResovle方法。

//在反序列化时,readResolve方法,则直接返回该方法指定的对象

privateObject readResolve(){

returngetInstance();

    }

测试结果:

image

<figcaption></figcaption>

序列化没有再破坏单例,而这一切JDK是如何处理的呢?

publicfinalObjectreadObject()

        throws IOException, ClassNotFoundException

    {

if(enableOverride) {

returnreadObjectOverride();

        }

intouterHandle = passHandle;

try{

//关键代码,最终返回的是此方法返回的对象

Objectobj = readObject0(false);

            handles.markDependency(outerHandle, passHandle);

            ClassNotFoundException ex = handles.lookupException(passHandle);

//more code but not importent

继续深入,发现readObject0方法的关键代码如下

bytetc;

//取出文件的一个字节,判断读取的对象类型

while((tc = bin.peekByte()) == TC_RESET) {

            bin.readByte();

            handleReset();

        }

        depth++;

        totalObjectRefs++;

try{

switch(tc) {

caseTC_NULL:

returnreadNull();

caseTC_ENUM:

returncheckResolve(readEnum(unshared));

//判断为对象类

caseTC_OBJECT:

returncheckResolve(readOrdinaryObject(unshared));

//more othrer case

继续追踪readOrdinaryObject方法,发现readReslove的关键代码

        //判断是否有readReslove方法(desc.hasReadResolveMethod())

if(obj != null &&

            handles.lookupException(passHandle) == null &&

            desc.hasReadResolveMethod())

        { 

          //执行readReslove

Objectrep= desc.invokeReadResolve(obj);

if(unshared &&rep.getClass().isArray()) {

rep= cloneArray(rep);

            }

if(rep!= obj) {

                // Filter the replacement object

if(rep!= null) {

if(rep.getClass().isArray()) {

filterCheck(rep.getClass(), Array.getLength(rep));

}else{

filterCheck(rep.getClass(),-1);

                    }

                }

                //最终返回readReslove方法的执行结果

handles.setObject(passHandle, obj =rep);

            }

        }

returnobj;

枚举单例 - 最完美的单例模式

大神Josh Bloch在《Effective Java》中极力推荐使用枚举的方式来实现单例。

package demo.single;

publicenumEnumSingle{

    SINGLE;

    public void doJob(){

System.out.println("doJob");

    }

}

枚举类型是单例模式的最佳选择,主要得益于JVM对于枚举类型的支持:

JVM保证枚举类型的每个实例仅存在一份

枚举类型的序列化与反序列化不会破坏其单例的特性(上面的源码大家可以去找一找)

反射也不能破坏枚举单例

可以说,枚举天然就是单例的,那么你会选择枚举作为单例吗?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339