面试官:你简历上有熟悉设计模式,那你给我说一下单例模式实现及线程安全吧

面试官:简历写你熟悉设计模式,给我说说单例模式实现及线程安全吧

前言

单例应用的太广泛,大家应该都用过,本文主要是想聊聊线程安全的单例以及反序列化破坏单例的情况。

1、概念

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

关键点:

  1. 私有化构造函数
  2. 通过一个静态方法或枚举返回单例类对象
  3. 确保单例类的对象有且只有一个,尤其是多线程环境下
  4. 确保单例类对象在反序列化时不会重新构建对象

2、实现

2.1、线程安全的单例

2.1.2、饿汉模式

饿汉模式:不管有没有调用getInstance方法,只要类加载了,我就给你new出来(a)

public class A {

    private static final A a = new A();

    public static A getInstance() {
        return a;
    }

    private A() {}
}

以下两点保证了以上代码的线程安全:

  1. 调用一个类的静态方法的时候会触发类的加载(如果类没加载过)
  2. 类只会加载(被加载到虚拟机内存的过程,包括5个阶段)一次
  3. static变量在类初始化的时候(类加载过程的最后一个阶段)会去赋值静态变量

2.1.2、懒汉模式

懒汉模式:延迟加载,用到再去new

public class B {
    private static volatile B b;

    public static synchronized B getInstance() {
        if (b == null) {
            b = new B();
        }
        return b;
    }

    private B() { }
}

要保证线程安全,最简单的方式是加同步锁。synchroized保证了多个线程串行的去调用getInstance(),既然是串行,那就不会存在什么线程安全问题了。但是这实现,每次读都要加锁,其实我们想要做的只是让他写(new)的时候加锁。

2.1.3、Double Check Lock (DCL)

public class B {
    private static volatile B b;

    public static synchronized B getInstance0() {
        if (b == null) {
            synchronized (B.class) {
                b = new B();
            }
        }
        return b;
    }

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

    private B() { }
}

为了解决懒汉模式的效率问题,我们改造成getInstance0(): 但还有个问题 X、Y 两个线程同时进入if (b == null), X先进同步代码块,new了一个B,返回。Y等到X释放锁之后,它也进了同步代码块,也会new一个B。

getInstance0()解决了效率问题,但它不是线程安全的。我们又进行了一次改造: getInstance():

getInstance在同步块里面,又做了一次if (b == null)的判断,确保了Y线程不会再new B,保证了线程安全。

getInstance() 也正是所谓的双重检查锁定(double checked locking)。

这里还有一个关键点: private static volatile B b; b是用volatile修饰的。 这个主要是因为new 并不是原子的。

B b = new B();

可以简单的分解成一下步骤

  1. 分配对象内存
  2. 初始化对象
  3. 设置引用指向分配的内存地址

2,3 直接可能发生指令重排序,就是说对象还未初始化完成,就让b指向了一块内存地址,这时候b就不是null了。

2.1.4、静态内部类单例模式

public class C {
    private C() {}

    public static C getInstance() {
        return CHolder.c;
    }

    private static class CHolder {
        private static final C c = new C();
    }
}

静态内部类的线程安全也是由jvm保证的,在调用Cholder.c的时候,去加载CHolder类,new 了一个c。

总的来说,这个方式比DCL还是高点的,因为DCL加了volatile,效率上还是略微有些些影响。

上面介绍的3种线程安全的单例,在有种极端的情况,单例模式有可能被破坏:反序列化

  • Java序列化就是指把Java对象转换为字节序列的过程
  • Java反序列化就是指把字节序列恢复为Java对象的过程。

反序列化的时候,会重新构造一个对象,破坏单例模式。我们看下代码验证下:

public class C1 implements Serializable {
    private C1() {
        System.out.println("构造方法");
    }

    public static C1 getInstance() {
        return CHolder.c;
    }

    private static class CHolder {
        private static final C1 c = new C1();
    }
// 注意这块被注释的代码
//    private Object readResolve(){
//        System.out.println("read resolve");
//        return CHolder.c;
//    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        C1 c = C1.getInstance();
        System.out.println(c.toString());
        try {
            ObjectOutputStream o = new ObjectOutputStream(
                    new FileOutputStream("d:/tmp/c.out"));
            o.writeObject(c);
            o.close();
        } catch(Exception e) {
            e.printStackTrace();
        }

        C1 c1 = null, c2 = null;

        try {
            ObjectInputStream in =new ObjectInputStream(
                    new FileInputStream("d:/tmp/c.out"));
            c1 = (C1)in.readObject();
            in.close();
        } catch(Exception e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream in =new ObjectInputStream(
                    new FileInputStream("d:/tmp/c.out"));
            c2 = (C1)in.readObject();
            in.close();
        } catch(Exception e) {
            e.printStackTrace();
        }

        System.out.println("c1.equals(c2) : " + c1.equals(c2));
        System.out.println("c1 == c2 : " + (c1 == c2));
        System.out.println(c1);
        System.out.println(c2);
    }
}

结果:

构造方法
me.hhy.designpattern.singletonpattern.C1@1540e19d
c1.equals(c2) : false
c1 == c2 : false
me.hhy.designpattern.singletonpattern.C1@135fbaa4
me.hhy.designpattern.singletonpattern.C1@45ee12a7

放开注释的代码

构造方法
me.hhy.designpattern.singletonpattern.C1@1540e19d
read resolve
read resolve
c1.equals(c2) : true
c1 == c2 : true
me.hhy.designpattern.singletonpattern.C1@1540e19d
me.hhy.designpattern.singletonpattern.C1@1540e19d

正如我们看到的那样,加上readResolve就解决了反序列化单例被破坏的问题。

当然,如果没实现Serializable接口,也就不会有这个被破坏的问题… 还是看场景。

关于readResolve的介绍,感兴趣的同学们可以看java.io.ObjectInputStream#readUnshared方法上的注释(博主看了,看得不是很明白,一知半解,就不误人子弟了)

而我们下面要介绍的枚举单例,并不会有这个问题。

2.1.5、枚举单例

public enum  DEnum {

    INSTANCE;

    private D d;

    DEnum() {
        d = new D();
    }

    public D getInstance() {
        return d;
    }
}
public class D {}

线程安全的保证:

  • 枚举只能拥有私有的构造器
  • 枚举类实际上是一个继承Enum的一个final类
  • 上面的INSTANCE实际是被static final 修饰的

序列化不破坏单例的保证:

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

2.2 线程不安全的单例

2.2.1、懒汉模式

不过多介绍了,这个其实在线程安全的单例部分,我们介绍的比较详细了。

public class B {
    private static volatile B b;

    public static B getInstance() {
        if (b == null) {
            b = new B();
        }
        return b;
    }

    private B() { }
}

3. 总结

单例的应用实在是太多了,也没必要再去找源码中的经典使用(因为基本上大家用过)。

枚举单例构造方法还是public,并不是防止外部直接去new它。个人认为如果一个类要开放给外部使用,用内部类的形式实现单例是最合适的。

来源:https://www.tuicool.com/articles/EV3eEnA

推荐关注:微信公众号【慕容千语】

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