【设计模式】- 单例模式

简介

单例模式可以说在平时开发过程中经常用到。当整个应用进程只需要创建一次某对象时。那么单例模式就派上用场了。很多人觉得单例模式很简单,但是里面有些细节和不同写法的差别,以及不同写法都解决了什么问题。还是可以了解了解。这样在开发过程中可以根据实际情况来使用单例模式。

UML

【设计模式】- 单例模式.png

难点

单例,单例——那么重点就是如何保证单例类的对象在整个应用进程有且只有一个。比如在多线程情况下,异步和原子操作带来的执行顺序的差异等等。

单例1

  • 饿汉模式
    public class Singleton {
        private static final Singleton INSTANCE = new Singleton();
        private Singleton() {}
        public static Singleton instance() { return INSTANCE; }
    }
    

这种算是最简单的一种写法,利用虚拟机只会对类加载一次,而静态变量会在类加载时进行初始化来保证单例类对象在整个应用进程期间有且只有一个。但是缺点就是:类加载便会立即创建单例对象,不管单例对象后面是否会被使用,会导致一定的资源浪费。

  • 懒汉模式
    class Singleton1 {
      private static class LazyHolder {
          private static final Singleton1 INSTANCE = new Singleton1();
      }
      private Singleton1 (){}
      public static final Singleton1 getInstance() {
          return LazyHolder.INSTANCE;
      }
    }
    

反序列化导致单例模式失效

让Singleton实现Serializable接口。编写测试程序。

private static void testSingleton() {

    Singleton singleton = Singleton.instance();
    try {
        FileOutputStream fos = new FileOutputStream(new File("singletonTest.txt"));
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(singleton);
        fos.close();
        oos.close();
        System.out.println("old:" + singleton.hashCode());

        FileInputStream fis = new FileInputStream(new File("singletonTest.txt"));
        ObjectInputStream ois = new ObjectInputStream(fis);
        Singleton newSingleton = (Singleton) ois.readObject();
        fis.close();
        ois.close();
        System.out.println("new:" + newSingleton.hashCode());

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

打印结果:

old:2016447921
new:960604060

可以发现,前后并不是同一个对象。那么是怎么造成的呢。看一下ObjectInputStream的readObject()方法。

    public final Object readObject()
        throws IOException, ClassNotFoundException
    ...
        try {
            Object obj = readObject0(false);
            ...
            return obj;
        } finally {
            ...
    }

这里obj便是序列化出来的对象。查看readObject0方法。

private Object readObject0(boolean unshared) throws IOException {
   ...
   try {
      switch (tc) {
          case TC_OBJECT:
             return checkResolve(readOrdinaryObject(unshared));
          ...
      }
   ...
}

序列化对象由readOrdinaryObject(unshared)获得,继续向下看。

    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
       ...
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            ...
        }
        ...
        return obj;
    }

重点就在obj = desc.isInstantiable() ? desc.newInstance() : null这行代码了。

  1. isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。
  2. desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。
    所以如果不做任何处理,序列化出来的是新创建的对象。
解决办法

增加readReslove方法,该方法会在反序列化时调用,且返回一个对象,这个对象就是readObject返回的对象。

public class Singleton implements Serializable {

    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static Singleton instance() { return INSTANCE; }
    private Object readResolve() {
        System.out.println("readResolve被调用了");
        return INSTANCE;
    }
    private Object writeReplace() {
        System.out.println("writeReplace被调用了");
        return INSTANCE;
    }
}

打印结果

writeReplace被调用了
old:1229416514
readResolve被调用了
new:1229416514

单例2

class Singleton2{
    private static Singleton2 singleton2;
    private Singleton2() {}
    public static Singleton2 instance(){
        if (null == singleton2){
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}

这种但是模式不适合运用到多线程情况,当使用场景不是在多线程时,那么这种写法的性能比第三种单例写法高。

单例3(双重检测)

class Singleton3{
    private static Singleton3 singleton3;
    private Singleton3() {}
    public static Singleton3 instance(){
        if (null == singleton3){
            synchronized (Singleton3.class){
                if (null == singleton3){
                    singleton3 = new Singleton3();
                }
            }
        }
        return singleton3;
    }
}

这种方式有叫“双重检测”,即检测两次是否为null。并用synchronized解决多线程带来的问题,那么问什么用两次判null,为什么不把synchronized写到方法体或者最外层呢?

  1. 为什么不把synchronized写到方法体或者最外层呢?
    如果singleton3不为null,那么就不会执行为null情况下的同步代码,要知道代码同步需要获取锁,释放锁等操作,是个繁重的过程,所以这样可以提高效率。
  2. 为什么还要进行第二次判null?
    想象这种情况,有A,B两个线程同时调用instance获取对象。线程A获取锁进入第二层判null代码,进程创建单例对象,但是这个过程并不是原子操作,什么时候创建完,并不知道。在没有创建完成的情况下,线程B到达,由于单例对象还未创建成功,所以线程B到达时,顺利通过第一层判null,然后试图获取锁进入第二层判null,这是发现锁被A持有,那么等待A释放锁,当单例对象创建成功,A也释放锁,这是B进入同步代码块,如果不进行第二次判null,那么B也会重新创建一个对象。

那这种方式能够保证单例的唯一性吗?其实并不能,由于java内存模型的缘故,"双重检测"并不能保证单例的唯一性。先补一下“java内存模型”知识。

Java内存模型

参考《深入理解Java虚拟机》一书。

  • 主内存和工作内存
    Java内存模型规则所有的变量都存储在“主内存”,每条线程拥有自己的“工作内存”,线程的“工作内存”保存了被线程使用到的变量的“主内存”副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在“工作内存”中进行,而不能直接读写“主内存”中变量。不同线程之间也无法直接访问对方的“工作内存”中变量,线程直接传递需要通过“主内存”来完成。
  • 内存间的交互操作
    关于“主内存”与“工作内存”直接的交互协议,Java内存模型中定义了以下8中操作来完成,虚拟机实现时必须保证下面提及的每一项操作都是原子的,不可再分的(对于double和long类型来说,load,store,read,write在某些平台有例外)。
    1. lock(锁定):作用于“主内存”的变量,它把一个变量标识为一条线程独占的状态。
    2. unlock(解锁):作用于“主内存”的变量,释放锁。
    3. read(读取):作用于“主内存”的变量,它把一个变量的值从“主内存”传输到线程的“工作内存”中,以便随后的load动作使用。
    4. load(载入):作用于“工作内存”的变量,它把read操作从“主内存”中得到的变量值放入“工作内存”的变量副本中。
    5. use(使用):作用于“工作内存”的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用的变量的字节吗指令时将会执行该操作。
    6. assign(赋值):作用于“工作内存”的变量,它把一个从执行引擎接收到的值赋给“工作内存”的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
    7. store(存储):作用于“工作内存”的变量,它把“工作内存中一个变量的值传输到“主内存”中,以便随后的write操作使用。
    8. write(写入):作用于“主内存”的变量,它把store操作从“工作内存”中得到的变量值放入“主内存”的变量中。
  • 内存间操作规则
    1. 不允许read和load,store和write操作之间单独出现即不允许一个变量从主内存读取了但工作内存不接受,反之一样。
    2. 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
    3. 不允许一个线程无原因地(没有发送过任何assign操作)把数据从线程的工作内存同步回主内存中。
    4. 一个新变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,即,对一个变量实施use,store操作之前,必须先执行过了assign和load操作。
    5. 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被一条线程重复执行多次,只有执行相同次数的unlock操作,变量才会被解锁。
    6. 如果对一个变量执行lock操作,那么将会清空工作内存中此变量的值,在执行引擎使用这个变量的前,需要重新执行load或者assign操作初始化变量值。
    7. 如果一个变量实现没有lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其它线程锁定的变量。
    8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中。
不完美的双重检测

有了上面的知识后,我们来试想一种场景。线程A,B同时调用instance获取实例,这时线程A,B的工作内存保存的是主内存中的副本,这是实例还没有创建,所有都为null,当线程A获得锁,进行实例创建,而B等待A释放锁,当A创建完对象,释放锁后,B获得锁进入第二层判null逻辑。重点来了,线程A的确完成了实例的创建,但是是在自己的工作内存,还没有同步到主内存,那么这时候线程B中的实例依然是null,所有还是会重新创建对象。解决办法可以给实例对象加上volatile关键字,下面补一下volatile关键字知识。

volatile
  • 保证变量对所以线程的可见性
  • 禁止指令重排序优化

单例4(枚举)

enum Singleton4{
    INSTANCE;
    public void test(){}
}

kotlin中单例模式

kotlin创建单例就很简单了,利用object 关键字即可而且对象声明的初始化过程是线程安全的。

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