单例终极分析(一)

单例的用处

若是你看过设计模式,确定会知道单例模式,实际上这是我能默写出代码的第一个设计模式,虽然很长一段时间我并不清楚单例具体是作什么用的。
这里简单提一下单例的用处。做为java程序员,你应该知道spring框架,而其中最核心的IOC,在默认状况下注入的Bean就是单例的。有什么好处?那些Service、Dao等只建立一次,没必要每次都经过new方式建立,也就不用每次都开辟空间、垃圾回收等等,会省很多资源。html

version 1: 饿汉式

那么如何写一个单例呢?我想不少朋友都能搞定:java

public class Singleton {

    private static final Singleton singletonInstance = new Singleton();    // A - 急不可待的成员变量赋值,static和final修饰
    private Singleton (){}    // B - 私有化的构造器,避免随意new

    public static Singleton getInstance(){    // C - 暴露给外部的获取方法
        return singletonInstance;
    }
}

Ok,拥有A、B、C三大特色(注释部分),就构成了著名的饿汉式单例。好处在于简单粗暴,易于理解(只要你真正通晓finalstatic的做用)。
但有豪放派,就有婉约派。后来你们都以为,我尚未使用这个类,你就直接把对象构建出来扔java堆里了,是否是有点不那么含蓄?程序员

因而你们快速迭代出懒汉式单例spring

version 2: 懒汉式

class Singleton {

    private static Singleton singletonInstance;     // A - 温婉到只有变量声明
    private Singleton (){}      // B 

    public static Singleton getInstance(){      // C 
        if(singletonInstance==null){
            singletonInstance = new Singleton();    // D - 成员变量的建立赋值延后至此
        }
        return singletonInstance;
    }
}

变化发生于A、D两步,总得来讲,就是把成员变量singletonInstance的建立和赋值延后了。基本的要求达到了,在没调用getInstance()方法以前,对象无建立,再也不麻烦java堆大大。一切看起来都很美好,但仅限于单线程状况下
好,看看你们喜闻乐见的并发场景下,这种简易的写法会出现什么问题——两个线程T-1T-2同时访问getInstance(),它们都以为singletonInstance==null判断成立,分别执行了步骤D,成功建立出singletonInstance对象!可是,咱们通篇都在聊单例啊,T-1T-2的玩法无疑很不单例!
问题分析出来了,而解决上并不复杂——让线程同步就好设计模式

version 2.1: 简易解决并发的懒汉式

class Singleton {

    private static Singleton singletonInstance;     // A
    private Singleton (){}      // B

    public static synchronized Singleton getInstance(){      // C - 用synchronized关键字修饰
        if(singletonInstance==null){
            singletonInstance = new Singleton();    // D
        }
        return singletonInstance;
    }
}

惟一的变化在于步骤C,加入了synchronized关键字,让线程同步执行此方法。如今问题解决了,无论线程T-1仍是T-2,在getInstance()面前都要小朋友们排排坐——一个个执行,这样即便是线程T-100甚至T-500过来也要排队执行,哈哈哈哈哈哈……呜呜呜……
既是解决方案,也是问题所在,这种方式效率太差了并发

咱们知道,synchronized有另外一种使用方式就是锁代码块,能够减小锁粒度。框架

class Singleton {

    private static Singleton singletonInstance;     // A
    private Singleton (){}      // B

    public static Singleton getInstance(){      
        synchronized (Singleton.class){    // C - 改为synchronized锁代码块
            if(singletonInstance==null){
                singletonInstance = new Singleton();
            }
        }
        return singletonInstance;
    }
}

但在这个例子中,该方式看上去彷佛没什么提高(该方法主要逻辑只有singletonInstance = new Singleton()一行)。好在有聪明人,研究出了Double-check性能

version 2.2: Double-check (有问题版)

class Singleton {

    private static Singleton singletonInstance;     // A
    private Singleton (){}      // B

    public static Singleton getInstance(){      
        if(singletonInstance==null){    // C1 - synchronized以前,第一次判断
            synchronized (Singleton.class){    
                if(singletonInstance==null){    // C2 - synchronized以后,第二次判断
                    singletonInstance = new Singleton();
                }
            }
        }
        return singletonInstance;
    }
}

我一直以为这种方式很巧妙。C1的判断用于非并发环境,阻拦对象建立后的大部分访问;C2的判断,解决首次建立对象时的并发问题。
很长一段时间,我以为这就是最终方案了,世界再次变得美好,没想到仍是图样图森破(too young, too simple!)。其实不止是单例,jdk1.5以前不少问题都被一个关键字耽搁了——volatile,而它相关的问题深深隐藏在Java内存模型层面,且听我缓缓道来……优化

version 2.3: volatile解决有序性

算了,照顾下没耐性的开发兄弟,先给出修改方案:this

class Singleton {

    private static volatile Singleton singletonInstance;     // A - 用volatile修饰
    private Singleton (){}      // B

    public static Singleton getInstance(){      
        if(singletonInstance==null){    // C1
            synchronized (Singleton.class){    
                if(singletonInstance==null){    // C2
                    singletonInstance = new Singleton();
                }
            }
        }
        return singletonInstance;
    }
}

能够看到,惟一的变化在于A位置加入了volatile关键字,用于解决有序性问题。volatile涉及的原子性可见性这里不做讨论)

有序性

什么是有序性?举个“栗子”:

int x=2;//语句1
int y=0;//语句2
boolean flag=true;//语句3
x=4;//语句4
y=-1;//语句5

对于上面的代码来讲,书写语句按顺序1至5,但执行上极可能不是这样。有多是1-4-3-2-5,或者1-3-2-5-4,其实只要保证1在4前而且2在5前,剩下的顺序能够随意变化。这要感谢内存模型同志,它自然容许编译器和处理器对指令进行重排序。动机是好的——能够默默的帮你作些优化,但在并发场景下,就有好心办坏事的嫌疑。

看下另外一个例子:

Context context = null;
boolean inited = false;

   //线程-1:
public void methodA(){
    context=loadContext();    //语句1
    inited=true;    //语句2
}

    //线程-2:
public void methodB(){
    while(!inited){
        sleep(1)    //语句3
    }
    doSomethingwithconfig(context);    //语句4
}

并发场景下,极可能出现以下状况:

clipboard.png

  • 线程-2语句3位置无忧无虑的休眠
  • 语句2语句1发生指令重排,线程-1进入methodA()时先执行语句2
  • 恰逢线程-2觉醒,执行语句4,此时context仍是null(语句1context初始化还没执行),灾难产生

volatile,是个“挡板”,能保证执行顺序。为何称之为“挡板”?还以以前的“栗子”说明:

int x=2;//语句1
int y=0;//语句2
volatile boolean flag=true;    //语句3 - 用volatile修饰
x=4;//语句4
y=-1;//语句5

语句3boolean变量 用volatile修饰后,重排只能分别发生在一、2之间或语句四、5之间。即语句一、2不能跨过语句3,语句四、5也不能跨过语句3

咱们还需知道,对于java的某些操做,好比++,虽然看上去是一行代码,但实质上这个操做自己并非原子的。以i++为例,该操做实际包含i的当前值获取,i+1计算,以及i=的赋值操做三兄弟。

一样的,singletonInstance = new Singleton()也非原子指令,包含:

  1. 对象内存分配
  2. 初始化LazySingleton对象属性
  3. 将singleton引用指向内存空间

若是不用volatile修饰,万恶的指令重排可能发生在步骤2步骤3之间,产生以下情况(此处有盗图嫌疑,罪过):

clipboard.png

以上图的状况,线程B获取到了还没有初始化彻底的LazySingleton对象,使得在后续的使用中出现异常! 用volatile修饰singleton变量后,指令重排技能被禁用,singletonInstance = new Singleton()只能按步骤一、二、3顺序执行,问题就此解决。

值得一提的是,其实存在更好的volatile修饰版本。

version 2.4:推荐的volatile + Double-check 版

class Singleton {

    private static volatile Singleton singletonInstance;     // A 
    private Singleton (){}      // B

    public static Singleton getInstance(){
        Singleton tempInstance = singletonInstance;    // C - 开启了临时变量
        if(tempInstance==null){    
            synchronized (Singleton.class){    
                if(tempInstance==null){
                    singletonInstance = tempInstance = new Singleton();
                }
            }
        }
        return tempInstance ;
    }
}

这种写法差异在于在代码C位置,声明了变量tempInstance临时变量,以后的逻辑都使用tempInstance代替singletonInstance。为何要这样作?wiki上准原文是这么说的:

Note the local variable "tempInstance ", which seems unnecessary. The effect of this is that in cases where singletonInstance is already initialized (i.e., most of the time), the volatile field is only accessed once (due to "return tempInstance ;" instead of "return singletonInstance;"), which can improve the method's overall performance by as much as 25 percent.

翻译一下就是:
singletonInstance对象大部分时候是已完成初始化的,用tempInstance临时变量以后能减小volatile属性(singletonInstance)的访问,这么作大概能提高25%的性能!

后续

哇,一不当心写了这么多,并且还没结束,留待下一篇吧。(主要是volatile部分比较罗嗦了,这个关键字各位需好好看下,借以窥探内存模型,原子性和可见性没作分析都已经占了这么大的篇幅)
下一篇文章会包含静态内部类实现单例final+泛型实现单例java9 VarHandler单例等,敬请期待!(会有人期待吗 ::>_<:: )

参考资料

相关文章
相关标签/搜索