12

单例的用处

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

version 1: 饿汉式

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

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堆里了,是不是有点不那么含蓄?

于是大家快速迭代出懒汉式单例

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解决有序性

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

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修饰后,重排只能分别发生在1、2之间或语句4、5之间。即语句1、2不能跨过语句3,语句4、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()只能按步骤1、2、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单例等,敬请期待!(会有人期待吗 ::>_<:: )

参考资料


青鱼
268 声望25 粉丝

山就在那里,每走一步就近一些