只是想写个单例模式

遥想当年,在西南某大学校花下,手捧《设计模式》的时候,玩得最6的就是单例模式了。想起那几行代码就能够得满分的那种感觉啊,再伴着校花的芳香,简直...了...

说写就写

单例模式,故名思议就是实例在任何时候只有一个,有时候需要一个类在任意时刻只存在一个实例,比如你看到的太阳和我看到的太阳就是同一个,不多说,来敲代码。

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

单例模式,要满足的是实例只有一个,既然只能一个,那就不能够随便实例化,这里将构造方法进行私有化,那么在外部就无法调用构造器进行实例化了,那么只能在类内部去调用构造器进行实例化,再提供一个获取实例的静态方法,这样基础的单例模式也就成了。

你以为的单例还不是单例

what?上面实现的不是单例模式那还能是什么?仔细想想,单例模式就是要控制对象只能创建一个,上面通过将构造器私有化来保证构造器被别人调用,但是想想对象会不会通过别的途径来创建呢? 对象创建的途径有四种,构造器、克隆、反序列化、反射,那我们就得保证所有途径都不会产生多余的实例。

首先构造被私有化了,问题没了,克隆需要实现对应的接口Cloneable 才能够进行,都要单例了,克隆功能也就可以排除不用了。

但是反序列化可不能随便去掉,反序列化这么好的东西总不能因为实现单例模式就给抛弃掉吧。那又如何处理呢?首先类实现了接口(Serializable)可以通过反序列化来生成一个对象,我们要保证的就是在单例模式下,反序列化生成的对象不会破坏我们的单例,这也不用我们保证,Java为我们保证了这一切。

public class Singleton implements Serializable {

      private static final long serialVersionUID = 1L;  
      private static Singleton instance = null;
      private Singleton(){....}
      public static Singleton getInstance(){
           if(instance == null){
                   instance = new Singleton();
           }
           return instance;
      }
      private Object readResolve() {return singleton;}
}

对比第一段代码和第二段代码,在加了Serializable 接口以外我们还增加了方法readResolve(),这个方法在Serializable 接口中是这样描述的:“Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.” 大概意思就是说可以通过这个方法返回一个实例来替代从流中读取的实例了。这样便解决了反序列化对单例模式的破坏。

还有反射,我们知道发射可以通过使用newInstance()方法来创建一个实例,但是有人会说,newInstance也是调用的构造方法进行实例,现在构造方法为私有的,怎么能够调用呢?别忘了反射是个强大的功能,一句setAccessible(true)就可以调用你私有的构造器了。为了阻止这样的情况发生,那又该如何来写呢?实际上我们要做的就是防止构造器被多次调用,来看代码.

public class Singleton{
      private static Singleton instance = null;  
      private static flag = false;
      private Singleton(){
            if(flag == false){
                  flag = true;
                  ....
            }else{
                    throw new RuntimeException("不能乱来,实例已经创建好了,请走正门")
            }
      }
      public static Singleton getInstance(){
           if(instance == null){
                   instance = new Singleton();
           }
           return instance;
      }
}

实在想不出什么场景下会把已有的instance给清空,所以就不考虑构造器内部多次调用了。

这样这个单例模式看上去就没什么问题了,每调用一次先判断是否有实例,没有就创建,下次调用就有了,直接返回,通过以上的途径来保证不会重复创建对象。等等,会不会出现两次调用实例都没有的情况呢?

线程安全的单例模式才是王道

如果两个线程同时来请求调用getInstance(),会出现什么样的情况呢?首先在程序运行过程中,是一段一段的顺序执行我们的代码,多线程情况下如果两个线程同时执行到if(instance == null),那肯定不用多说这是会出问题的。那么我们需要一个线程安全的单例类,来保证在多线程情况下是没有问题的。

public class Singleton{
      private static Singleton instance = null;  
      private static flag = false;
      private Singleton(){
            if(flag == false){
                  flag = true;
                  ....
            }else{
                    throw new RuntimeException("不能乱来,实例已经创建好了,请走正门")
            }
      }
      public static synchronized Singleton getInstance(){
           if(instance == null){
                   instance = new Singleton();
           }
           return instance;
      }
}

在getInstance方法前加上synchronized就能够保证我们的单例类是安全的了,在每一个线程进行请求时,如果没有线程进入这个方法它就进入执行,有则等待,这样就没有人同时去执行if(instance == null)的判断了,但是这似乎又给我们带来了累赘,在绝大多数的时候,单例类的实例是已经创建好了的,我们只用去请求,这时候并不需要进行线程安全控制,不需要同步,用同步反而影响了多线程的运行效率,那又当如何改善呢,这也不是什么难事,Google一下你就知道有种叫double-check 的处理方式,如何处理呢,看名称就知道要进行两次检查,先看代码

public class Singleton{
      private static Singleton instance = null;  
      private static flag = false;
      private Singleton(){
            if(flag == false){
                  flag = true;
                  ....
            }else{
                    throw new RuntimeException("不能乱来,实例已经创建好了,请走正门")
            }
      }
      public static  Singleton getInstance(){
           if(instance == null){//A
                    synchronized(Singleton.class){
                            if(instance == null){//B
                                   instance = new Singleton();
                            }
                    }
           }
           return instance;
      }
}

这是何意呢,我们 来模拟一下线程执行就知道了 线程1和线程2同时调用了getInstance(),都判断出了Instance为null,线程1先进入了同步代码块,进行了锁定,开始实例化,线程2就等着,实例化完成以后,同步代码块取消锁定,线程2进入,注意这时候实例已经有了,不需要在进行创建了,所以这里要做第二次检查,这就是double-check的由来了。这样也算是相当完美的单例模式了,为啥是相当完美,因为有的前辈在使用的时候就遇到了JVM指令重排的坑了。

首先什么叫指令重排,概念什么的自行Google,大致就是说JVM为了优化执行可能会对程序指令进行重排,虽然程序指令执行顺序进行了改变,但是执行结果是必须得保持一致的,JVM如何来保证呢,就是仅对互相没有依赖关系的指令进行重排,这样就保证了执行结果一致。

这样的指令重排不会影响最终结果那为何还会是个坑呢,首先我们来看看坑在哪儿,注意Instance = new Singleton();这句代码,实际上,JVM对于这句代码是三个步骤。

     1. 分配内存
     2. 在分配好的内存中初始化实例
     3. 将内存地址引用赋值给instance

这三个步骤,2依赖于1,3依赖与1,但是2和3直接没有相互依赖,所以在JVM中可能对2、3步骤进行重排,执行顺序变成了1,3,2,这样的重排在单线程应用中也没有什么问题。但是在多线程中就不一样了。现在我们把一个线程在获取单例的重要步骤列举一下

     A. 判断Instance是否为null
     B. 为null 进行实例化
          1. 分配内存
          2. 在分配好的内存中初始化实例
          3. 将内存地址引用赋值给instance
     C. 不为null 直接返回

在指令重排的情况下,线程一的执行顺序为A->B->B1->B3->B2,当需要实例化的对象比较大时,线程一执行到B2,此时Instance已经有值了,就是B2正在初始化的那片内存;此时线程二再执行就直接是A->C,因为Instance已经有值了,这样问题就出来了,现在线程二获取到的实例是没有完成初始化的实例,这可是要出大事情的。

为了解决这个坑,在Jdk1.5以后,Java对内存模型做了修改,这样在JDK1.5+,volatile这个关键字就可以来解决这个问题了,也就是说在jdk1.5之前,double-check是有毛病的。现在我们来看看volatile是怎么样解决这个问题的,先了解一下volatile;volatile关键字的学问还是有一点大,在这里先讲讲解决这个问题所用的的特性,其他的下次专门分析分析;在这里volatile的作用就是禁止JVM指令重排,由volatile声明的变量时,将禁止JVM对这个变量的相关指令进行重排,也就是不会出现前面的那种情况了,操作起来特比简单,直接代码。

public class Singleton{
      private volatile static Singleton instance = null;  
      private static flag = false;
      private Singleton(){
            if(flag == false){
                  flag = true;
                  ....
            }else{
                    throw new RuntimeException("不能乱来,实例已经创建好了,请走正门")
            }
      }
      public static  Singleton getInstance(){
           if(instance == null){//A
                    synchronized(Singleton.class){
                            if(instance == null){//B
                                   instance = new Singleton();
                            }
                    }
           }
           return instance;
      }
}

就这样在instance = new Singleton();就不会发生指令重排了,这样单例模式也就可以了。

本文结合网络上各路大神所写的文章进行分析,并通过自己的理解进行文,难免会出现相同或者我理解有错误,欢迎批评指正,谢谢。

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

推荐阅读更多精彩内容