JAVA设计模式-单例模式(Singleton)

单例模式是设计模式中使用最普遍的设计模式之一。它是一种对对象创建模式,主要用于产生一个对象的具体唯一实例,它可以确保系统中一个类只产生一个实例。在Java语言中,这样的行为能带来两大好处:

   1. 对于频繁使用的对象,可以节省创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。
   2. 由于new操作的次数减少,因而对系统的内存使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。

因此对于系统的关键组件和被频繁使用的对象,使用单例模式便可以有效地改善系统的性能。下面简单实现了一个单例模式:

public class Singleton {
 static{
    System.out.println("Singleton is init!");
 }
 private Singleton(){
    //创建单例的过程可能会比较慢
    System.out.println("Singleton is create!");
 }
 private static Singleton instance = new Singleton();
 
 public static Singleton getInstance(){
    return instance;
 }
}

主要代码中的重点标注部分,首先单例类必须要有一个private访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化,这点是相当重要的;其次,instance成员变量和getInstance()方法必须是static的。
这种单例的实现方式非常简单,而且十分可靠。它唯一的不足仅是对instance实例做延迟加载。例如单例的创建过程很慢,而由于instance成员变量是static定义的,因此在JVM加载单例类时,单例对象就会被建立,如果此时,这个单例类在系统中还扮演其他角色,那么在任何使用这个单例类的地方都会初始化这个单例变量,而不管是否会被用到。比如单例类作为String工厂,用于创建一些字符串(该类即用于创建单例Singleton,又用于创建字符串对象)

public class Singleton {
 static{
    System.out.println("Singleton is init!");
 }
 private Singleton(){
    //创建单例的过程可能会比较慢
    System.out.println("Singleton is create!");
 }
 private static Singleton instance = new Singleton();
 
 public static Singleton getInstance(){
    return instance;
 }
public static void createString(){
  System.out.println("createString in Singleton!");
 }

当使用Singleton.createString()执行任务时,程序输出:

singleton is init!
singleton is create!
createString in Singleton!

从上面的输出结果可以看到,虽然此时并没有使用单例类,但它还是被创建了出来,这也许是开发人员所不愿意看到的。为了解决这个问题,并以此提高系统在相关函数调用时的反应速度,就需要引入延迟加载机制。

 public class LazySingleton{
        private LazySingleton(){
              //创建单例的过程可能会比较慢
              System.out.println("LazySingleton is create");
        }
        private static LazySingleton instance = null;
        public static synchronized LazySingleton getInstance(){
                if(instance == null)
                      instance = new LazySingleton();
                       return instance;
        }
  }

从上面的另一种延迟加载的单例写法可以看出,对于静态成员变量instance 初始值赋予null,确保系统启动时没有额外的负载。在getInstance()方法中,判断当前单例是否已经存在,若存在则返回,不存在则再建立单例。这里尤其还要注意,getInstance()方法必须是同步的,否则在多线程环境下,当线程1正新建单例时,完成赋值操作前,线程2可能判断instance为null,故线程2也将启动新建单例的程序,而导致多个实例被创建,故同步关键字是必须的。
使用上例中的单例实现,虽然实现了延迟加载的功能,但和第一种方法相比,他引入了同步关键字,因此在多线程环境中,它的时耗要远远大于第一种单例模式。让我们来测试一下。

    @Override
     public void main(){
          for(int i = 0;i<100000;i++){
              Singleton.getInstance();
              //LazySingleton.getInstance();
              System.out.println("spend time:"+(System.currentTimeMillis()-begintime));
          }
      }

开启5个线程同事完成以上代码的运行,使用第1种类型的单例耗时0ms,而使用LazySingleton却相对耗时约390ms。性能至少相差2个数量级。

注意:在不同计算机上其测试结果很可能与以上测试结果不同。我们大可不必关心测试数据的绝对值,只要观察用于比较的目标代码间的相对耗时即可。

为了使用延迟加载引入同步关键字反而降低了系统性能,是不是有点得不偿失呢?为了解决这个问题,还需要对其进行改进:

  public class StaticSingleton{
      private StaticSingleton(){
               System.out.println("StaticSingleton is create!");   
       }
       private static class SingletonHolder{
              private static StaticSingleton instance = new StaticSingleton();
       }
        public static StaticSingleton getInstance(){
                return SingletonHolder.instance;
        }
  }

在这个实现中,单例模式使用内部类来维护单例的实例,当StaticSingleton被加载时,其内部类并不会初始化,故可以确保当StaticSingleton类被载入JVM时,不会初始化单例类,而当getInstance()方法被调用时,才会加载SingletonHolder,从而初始化instance。同时,由于实例的建立是在类加载时完成,故对多线程天生友好,getInstance()方法也不需要使用同步关键字。因此,这种实现同时兼备以上两种实现的优点。
通常情况下,用以上方式实现的单例已经可以确保在系统中只存在唯一实例了。但仍然有例外情况,可能导致系统生成多个实例,比如,在代码中,通过反射机制,强行调用单例类的私有构造函数,生成多个单例。考虑到情况的特殊性,因此不准备再细说。下面主要说说还有些合法的方法,也可能导致系统出现多个单例类的实例。

 public class Singleton implements java.io.Serializable{
 static{
    System.out.println("Singleton is init!");
 }
 private Singleton(){
    //创建单例的过程可能会比较慢
    System.out.println("Singleton is create!");
 }
 private static Singleton instance = new Singleton();
 
 public static Singleton getInstance(){
    return instance;
 }
public static void createString(){
  System.out.println("createString in Singleton!");
 }
//阻止生成新的实例,总是返回当前对象。
private Object readResolve(){
        return instance;
}

测试如下:

 @Test
 public void test throws Exception{
  Singleton s1 = null;
  Singleton s = Singleton.getInstance();
  //先将实例串行化到文件
  FileOutputStream fos = new FileOutputStream("serSingleton.txt");
  ObjectOutputStream oos = new ObjectOutputStream(fos);
  oos.writeObject(s);
  oos.flush();
  oos.close();
  //从文件读出原有的单例类
  FileInputStream fis = new FileInputStream("serSingleton.txt");
  ObjectInputStream ois = new ObjectInputStream(fis);
  s1 = (Singleton) ois.readObject();
  Assert.assertEquals(s1,s);
 }

使用上述测试代码测试单例的串行化和反串行化,当去掉Singleton代码中readResolve方法后,测试代码会抛出异常警告,说明测试代码中的s和s1指向不同的实例,在反序列化后,生成了多个对象实例。而加上readResolve()方法后,程序正常退出。说明,即便经过了反序列化,仍然保持了单例的特征。事实上,在实现了私有的readResolve()方法后,readResolve()已经形同虚设,它直接使用readResolve()替换了原本的返回值,从而在形式上构造了单例。

注意:序列化和反序列化可能会破坏单例,一般来说,对单例进行序列化和反序列化的场景并不多见,但如果存在,就要多加注意,小心使用啦!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容