设计模式之你真的了解单例模式么?

问题思考

你知道什么是单例模式么?你能写出一个性能有保障并且安全的单例模式么?

首先我们先明确单例模式的概念,单例是指在整个JVM实例中该类只存在一个实例。

单例演进

一般了解java的同学都可以熟练的脱口而出,常用的两种单例模式,一种是饿汉式,一种是懒汉式

饿汉式
public class Singleton {
    
    /**私有化无参数构造器*/
    private Singleton(){}
    
    /**声明为static final 常量*/
    private static final Singleton INSTANCE = new Singleton();
    
    /**获取实例*/
    public static Singleton getInstance (){
        return INSTANCE;
    }

}

以上便是饿汉式单例的常规写法;那么如果反序列化对象,重新生成一个实例,那么在JVM中该类的实例是指向同一个内存地址么?这还是单例吗?

/**我们给以上类实现Serializable接口,执行以下代码*/
public static void main(String[] args) {
    Singleton instance = Singleton.getInstance();
    try {
        /**将该对象序列化输出*/
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("single.obj"));
        out.writeObject(instance);
        out.flush();
        out.close();

        /**将该对象反序列化读入*/
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("single.obj"));
        Singleton instance2 = (Singleton) in.readObject();
        in.close();

        System.out.println("instance 与 instance2 是同一个实例: " + (instance == instance2));
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

执行结果是instance 与 instance2 是同一个实例: false,表明反序列化回来的与对象不是同一个实例;那么该如何保证反序列化回来仍为原实例呢?我们可以通过如下方式实现

在原有Single类中实现readResolve方法
public class Singleton implements Serializable {

    /**私有化无参数构造器*/
    private Singleton() {
    }

    /**声明为static final 常量*/
    private static final Singleton INSTANCE = new Singleton();

    /**获取实例*/
    public static Singleton getInstance() {
        return INSTANCE;
    }

    /**解决反序列化问题*/
    public Object readResolve() {
        return INSTANCE;
    }

}

新增readResolve 方法后,我们再次执行main方法,发现返回的结果如下:

instance 与 instance2 是同一个实例: true

所以我们针对反序列化而产生的实例不唯一的问题都可以通过这种方式来解决,下文将不再赘述。

懒汉式

我们紧接着再回顾一下常用的懒汉式写法

public class Singleton {

    /**私有化构造函数*/
    private Singleton() {
    }

    /**声明私有 static 实例*/
    private static Singleton instance;

    /**声明公有的 获取实例方法*/
    public static Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
    
}

以上便是懒汉式写法,但是这样的写法在高并发的情况下,没有同步机制的保证,是线程不安全的,有可能会存在多个实例;那么我们怎么升级一下呢?

懒汉式升级之同步锁版
public class Singleton {

    /**私有化构造函数*/
    private Singleton() {
    }

    /**声明私有 static 实例*/
    private static Singleton instance;

    /**声明公有的 获取实例方法 使用synchronized的关键字限制*/
    public static synchronized Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
    
}

我们发现升级后的单例模式在getInstance方法上添加了synchronized关键字,实现了线程安全,那么在安全性能上没有什么问题,但是方法只能单线程访问,在效率上得不到保证;聪明的同学可能已经想到了双检锁的写法,我们一起来看看

懒汉式升级之上双检锁版
public class Singleton {

    /**私有化构造函数*/
    private Singleton() {
    }

    /**声明私有 static 实例*/
    private static Singleton instance;

    /**声明公有的 获取实例方法*/
    public static Singleton getInstance() {
        /**第一次检查*/
        if (null == instance) {
            synchronized(Singleton.class){
            /**第二次检查*/
            if (null == instance) {
                instance = new Singleton();
            }
          }
        }
        return instance;
    }
    
}

升级过后的我们的代码看上去完美无瑕,但是爱钻牛角尖的“独秀”同学可能会说,这个还是有一些问题,具体是什么问题呢我们一起来研究研究。

原来是这样的,在 instance = new Singleton();;JVM对于此处的操作并不是原子操作;具体分为三个步骤:

1.为instance对象开辟内存空间

2.调用构造函数初始化成员变量

3.将对象引用insatnce指向该内存空间(PS:此步骤后instance将不等于null)

由于此处的不是原子操作,JVM为了提升CPU的执行效率,会进行指令重排;造成这三步的执行顺序有可能为1-2-3、1-3-2;在后者发生的情况下,如果线程A步骤3执行之后,步骤2未执行之前;步骤2被线程B占用,这是instance不为null,但是却没有初始化;线程会正常返回,在后续的使用过程中由于没有初始化会造成出错。

那么针对这种情况,解决的思路也很明确,就是防止JVM指令重排,我们使用volatile关键字声明instance即可。

public class Singleton {

    /**私有化构造函数*/
    private Singleton() {
    }

    /**声明私有 static 实例,使用volatile 防止JVM指令重排*/
    private static volatile Singleton instance;

    /**声明公有的 获取实例方法*/
    public static Singleton getInstance() {
        /**第一次检查*/
        if (null == instance) {
            synchronized(Singleton.class){
            /**第二次检查*/
            if (null == instance) {
                instance = new Singleton();
            }
          }
        }
        return instance;
    }
    
}

虽然以上写法保证了线程安全;但是需要注意的是在JDK5之前使用双检锁volatile版本还是有问题的,因为在JDK5之前的JVM内存模型有缺陷,即使使用volatile关键字,也不能完全的防止指令重排;所以我们又有了其他版本的单例模式。

静态内部类写法
public class Singleton {
    
    /**私有化构造函数*/
    private Singleton() {
    }
    
    /**声明公有的 获取实例方法*/
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
    /**私有内部静态类*/
    private static class SingletonHolder {
    
      private static final Singleton INSTANCE = new Singleton();
      
    }
  
    
}

这种写法没有JDK版本的限制,并且静态内部类SingletonHolder是私有的,只有getInstance可以方法,所以它也是懒汉式的;同时读取时也没有限制,是线程安全的;这种方式也是《effective java》推荐的。

枚举单例写法
public enum Singleton{

    INSTANCE;
    
}

这种写法简单,并且enum实现也是线程安全的,不需要担心双检锁;获取方式直接通过Singleton.INSTANCE,比getInstance简单;还可以防止反序列化。

延伸思考

最秀的永远是“独秀”同学,当我们通过反射的手段获取的对象实例跟原有实例是同一个实例么?为什么?

public static void main(String[] args) {
    try {
        SingletonReflect instance = SingletonReflect.getInstance();
        Class clazz = Class.forName("xin.sunce.pattern.SingletonReflect");
        Constructor constructor = clazz.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        SingletonReflect instance2 = (SingletonReflect) constructor.newInstance();

        System.out.println("instance 与 instance2 是同一个实例: " + (instance == instance2));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

执行结果是:
instance 与 instance2 是同一个实例: false

为此,我们可以通过一定的手段防止像“独秀”一样的同学走后门。

/**
* 静态内部类限制方式
* 修改私有构造函数,防止走后门
*/
private Singleton() {
    if (null != SingletonHolder.INSTANCE) {
        throw new RuntimeException();
    }
}

/**
* 双检锁方式
* 修改私有构造函数,防止走后门
*/
private Singleton() {
    if (null != instance) {
        throw new RuntimeException();
    }
}

读者可以根据实际场景结合具体需求来判断适合使用哪种单例模式,以及要对对应的单例模式做到心知肚明。

总结

阅读完本文以后烦请你思考一下问题,有助于巩固理解:
1.思考你的单例是否是线程安全的
2.知晓你的单例在并发场景下会不会阻塞
3.你的单例经得起反序列化的考验么
4.你的单例经得起反射的考验么

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

推荐阅读更多精彩内容