死磕-单例模式

单例模式,可能是唯一一个我们谈到时,每个工程师都会二眼放光,滔滔不绝的模式,除了它最简单直接外,还因为我们“自以为”对它了如指掌,这篇文章带大家做个总结,死磕单例模式的方方面面。

单例的需求由来

大概有以下二种场景需要单例

  • 有些对象只应该存在一个,比如 “上帝” “女娲”等等,天然具有单一的性质
  • 我们造出来的配置类,管理类,比如UserManager,ServiceManager等等,只需维护一处,可以全局引用

单例的四种实现

实现一个单例,我们要考虑以下几点。

  • 如何防止外部调用new关键字来创建新的对象
  • 如何做到防止通过对象序列化来创建新的对象
  • 实现了cloneable的类,如何防止clone来创建新的对象
  • 如何防止反射调用构造器来创建新的对象
  • 如何做到线程安全

总结一句话,如何线程安全的创建唯一实例对象。
先看一下Java中如何具体实现单例。

单例实现-懒汉模式

public class UserManager {
    private static UserManager instance = new UserManager();
    private UserManager() { }
    public static UserManager getInstance() {
        return instance;
    }
}

首先通过私有化构造器,禁止了外部new的可能性,然后instance是static修饰的,所以在类被首次加载后,调用init 的时候,instance会被初始化,JVM保证类加载过程的线程安全,所以instance也是线程安全的。
因为在类加载初始化的时候,单例就被创建出来了,所以相对于按需延时加载,这种写法如果有大量单例需要创建,在系统刚启动时内存压力比较大。同时上面的写法也没有能够禁止序列化和反射对单例的破坏(关于这个我们放到最后来解决)。

单例实现-Double Check

    private static volatile UserManager instance;

    private UserManager() {}

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

这也是很经典的单例实现,通过二次判空检查,而且只有在第一次初始化时getInstance会加锁,后面的获取都不会加锁,时间和空间效率都很高。
这里要注意的一点是instance一定要加volatile修饰符。关于这一点,很多同学可能会理解的不够全面,下面我来详细分析一下。
首先因为在创建UserManager的时候,我们是有加锁的,而且锁的对象是UserManager这个Class对象。比如线程A获得了锁,开始new UserManager(), 并且赋值给了instance,这时候线程B开始调用getInstance()来获取单例对象,由于锁拥有可见性,所以线程A的赋值happen-before线程B的获取,表面上看一切很完美,但是在jdk1.5之前,volatile语意还没有被加强,不能禁止指令重排序。

instance = new UserManager();

这条语句,其实可以被看做三条伪代码。

  • alloc userManager (堆上分配内存)
  • userManager init (对象初始化)
  • instance = userManager
    注意,alloc必须首先执行,但是init 和第三条 赋值语句,JVM并没有做定义,也就是说如果不加volatile,它们可以被重排序。
    一旦被重排序,线程B在获取instance时,有可能获取到的instance还没有执行init,这就是一次很危险的调用。但是加上volatile关键字,在jdk1.5之后,就不会再有这个问题了。

单例实现-静态内部类

    private UserManager() {}
    
    private static UserManager getInstance() {
        return SingltonHolder.sInstance;
    }

    private static class SingltonHolder {
        private static UserManager sInstance = new UserManager();
    }

静态内部类的方式实现的单例同样是线程安全的,由JDK来保证。同时也具有延时加载的特性。这种写法对比Double-Check更简洁,推荐使用。

单例实现-枚举

Effective Java中推荐使用枚举的方式来实现单例,我们来看一下

public enum UserManager {
    INSTANCE;
}

很简洁,但我们知道,枚举是Java提供的语法糖,我们解语法糖看下它的具体代码

public final class com.dig.deep.design.singlton.UserManager extends java.lang.Enum<com.dig.deep.design.singlton.UserManager> {
  public static final com.dig.deep.design.singlton.UserManager INSTANCE;
  private static final com.dig.deep.design.singlton.UserManager[] $VALUES;
  public static com.dig.deep.design.singlton.UserManager[] values();
  public static com.dig.deep.design.singlton.UserManager valueOf(java.lang.String);
  private com.dig.deep.design.singlton.UserManager();
  static {};
}

可以看到解语法糖后的UserManager,构造器也是私有的,有个一个static final 的INSTANCE类常量,可以大胆猜测,JVM在加载枚举类时,会给所有的枚举项赋值,同时会保证过程的线程安全。

如何防止单例被破坏

我们上面有提到过,一个完整的单例需要做到防止

  • 对象序列化对单例语意的破坏
  • 反射对单例语意的破坏
  • clone对单例语意的破坏

解决对象序列化的问题

        try {
            UserManager userManager = UserManager.instance;
            FileOutputStream fileOutputStream = new FileOutputStream("user");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(userManager);

            FileInputStream fileInputStream = new FileInputStream("user");
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            UserManager newUserManager = (UserManager) objectInputStream.readObject();

            System.out.println("is equal: " + (userManager == newUserManager));
        } catch (IOException e) {
            e.printStackTrace();
        }

输出是false,经过序列化和反序列化后,生成了二个单例对象,显然破坏了单例的语意,解决这个问题,我们可以给UserManager增加一个readResolve方法, 并在其中返回单例对象。

    private Object readResolve() {
        return instance;
    }

解决反射的问题

        Class clazz = UserManager.class;
        Constructor[] constructors = clazz.getDeclaredConstructors();
        try {
            constructors[0].setAccessible(true);
            UserManager newUserManager = (UserManager) constructors[0].newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

如果开发者真的使用反射来作恶,谁能拦得住呢?虽然反射最终调用的还是我们的私有构造器,在构造器里面我们可以加一些判断逻辑,但是还是不能涵盖所有的情况,因为毕竟我们的单例实现多种多样,有延时加载的,有非延时加载的。
但是通过Enum方式实现的单例是不能够被反射的,如果尝试反射Enum的构造器,会抛出一个异常,所以Enum方式实现的单例对反射安全。

解决clone的问题

尽量不要给单例实现cloneable接口,如果非要实现,也在重写的clone方法里,返回此单例对象。

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

总结

单例模式比较简单,同时我们日常工作也用的很频繁,工程师有必要对它有个全面了解,在选择实现方案时做到心中有数。

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

推荐阅读更多精彩内容