小小的单例模式也能玩成这样

Posted 东邪日记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小小的单例模式也能玩成这样相关的知识,希望对你有一定的参考价值。


计算机中,有一个学科叫:设计模式,灵感来源于建筑学。设计模式中有一个比较重要的模式:单例模式。毫不夸张地讲,几乎所有学过计算机的人都知道这个模式,觉得这个模式比较简单。可是我想,正是因为这个模式的使用太普遍了,才让我们误以为这个模式简单到令人发指。从这个简单的模式中,其实能引出很多知识点:

  1. 多线程

  2. 加锁机制

  3. 对象初始化过程

  4. 静态内部类

  5. 枚举

  6. 即时编译器


那怎么理解这个单例模式呢?


用大白话简单定义就是:一个系统中,如果只能允许一个对象存在,则称这种情况是单例模式。比如,

  1. 在一夫一妻制的社会中,一个家庭只能允许有一个父亲(母亲)角色的存在,若存在多个,就可能(一定)出问题;

  2. 在一个公司中,只能有一个CEO,否则在公司治理上,也可能(一定)出问题,等等。


如何实现这个单例模式呢?


考虑的角度不同或者附加的维度不同,将需要不同的实现方式,但是都必须满足的一个前提是:不允许外部构造(构造器私有化),只能内部构造,并提供对外调用方法。


如果系统是单线程的,则简单的一种实现方式如下:

public class Singleton {  
  public static final Singleton INSTANCE = new Singleton();

  private Singleton() {}
}

如果考虑到类加载的效率问题(上述实现方式,是在类被虚拟机加载的时候进行实例化操作),则需要延迟类的实例化,实现方式如下:

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

在单线程环境中,以上实现方式,已经做到了简单明了,写法优雅。但是,如果换个角度,考虑到多线程环境,以上的实现方式就会出大问题:不能实现我们想要的“单例”效果。


举个例子,如果一个父亲有多个孩子(双胞胎也行),夜里,这几个孩子同时哭了,都需要父亲喂奶,假如孩子们自己可以朝着这个“系统”索要父亲,如果用上述的实现方式,则导致系统中被构造了多个父亲。为什么会发生这种情况呢?

因为当多个孩子“线程”同时调用getInstance()方法时,都有可能判断出父亲实例都是null,此时,都会走instance = new Singleton()这段代码,这样的话,就构造出了多个父亲实例,而系统中出现多个父亲,就出了大问题了。那如何改造以上代码呢?


按正常思维思考,我们都会想到:加把锁。当一个线程通过后,立马把门锁起来,不允许其他线程通过,等到屋子里的线程处理完成之后,再把锁打开,放正在门外等候的某一个线程通过,以此类推。


当想到加锁之后,剩下来的问题是:把锁加在哪儿呢?简单的方法是加到getInstance()方法上,实现方式如下:

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


但是,人类永远是不满足的,尤其是对效率的追求。 上述实现方式,有个大问题就是:每次调用getInstance()这个方法时候,都得开锁,这大大牺牲了效率。所以,我们就想呀,能不能把加锁的时机延迟,粒度细化?


当然是可以的:给代码块加锁,实现方式如下:

public class Singleton {  

    private static Singleton instance = null;  

    private Singleton() {}  

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

我们又想,还能不能再延迟一点,粒度再细化一点,如下

public class Singleton {  
    private static Singleton instance = null;  

    private Singleton() {}  

    public static Singleton getInstance() {    

    if (instance == null) {
        synchronized(Singleton.class) {
            instance = new Singleton();      
        }    
    }    
    return instance;
  }
}

但是,可惜的是,通过分析(省略),以上实现方式是有问题的,存在两个问题:

第一个问题

instance = new Singleton()这句代码不是“原子性操作”,这句代码虽然简短,却需要虚拟机干三件事情:

1) 给对象分配内存

2) 调用 Singleton 的构造函数来初始化成员变量,形成实例

3) 将对象指向分配的内存空间


而上述三件事情,执行顺序不可确定,因为存在指令重排的优化,也就是说,当多个线程同时执行if(instance==null)时,对象可能还没有初始化完成,但是判断结果是false,直接返回了,造成紊乱。那有没有什么方法解决呢?在JVM1.5以及以上版本,使用volatile关键字可以禁止指令重排。


第二个问题

对象未被实例化完成之前,当多个线程同时到达代码if(instance == null)处,判断都将是true,也就是说,都将进入if代码块,实例化对象,这将导致对象被重复实例化。解决方法是:在同步块再进行一次判空处理。


代码实现如下:

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

这种实现单例模式的方式被称为双重检查。个人觉得这种实现方式最大的问题有三点:实现起来复杂、代码不优雅、受虚拟机版本限制。我想,也正是因为这样,大神Joshua Bloch在《Effective Java》这本书第二版中描述的单例模式是如下两种:


第一种:静态内部类实现单例

public class Singleton { 
  private static class SingletonHolder { 
    private static final Singleton INSTANCE = new Singleton();   
  } 
  private Singleton() {} 

  public static Singleton getInstance() { 
    return SingletonHolder.INSTANCE; 
  } 
}


第二种:枚举实现法

public enum Singleton {
    INSTANCE;
}

上述两种实现方法,从线程安全、代码优雅角度来说,都是近乎完美的(涉及到两个知识点的理解:静态内部类、枚举)。


最后,希望我理解单例模式的方式,能系统化那些杂七杂八的单例模式介绍!


后续思考:

1. 如果在不同的自定义类加载器(Class Loader)中,该如何保证单例呢?

2. 如果在不同的虚拟机中,该如何保证单例呢?

3. 如果对象是通过反序列化生成,又改如何保证单例呢?


--EOF--


推荐阅读:

以上是关于小小的单例模式也能玩成这样的主要内容,如果未能解决你的问题,请参考以下文章

简单的单例模式其实也不简单!

性能比较好的单例写法

如何写一个简单的单例模式?

你见过这样的单例模式吗?

片段作为 Android 中的单例

C++11标准下的单例设计模式