资讯专栏INFORMATION COLUMN

单例终极分析(一)

Jenny_Tong / 2060人阅读

摘要:好,看看大家喜闻乐见的并发场景下,这种简易的写法会出现什么问题两个线程和同时访问,它们都觉得判断成立,分别执行了步骤,成功创建出对象但是,我们通篇都在聊单例啊,和的玩法无疑很不单例问题分析出来了,而解决上并不复杂让线程同步就好。

单例的用处

如果你看过设计模式,肯定会知道单例模式,实际上这是我能默写出代码的第一个设计模式,虽然很长一段时间我并不清楚单例具体是做什么用的。
这里简单提一下单例的用处。作为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
}

并发场景下,很可能出现如下情况:

线程-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()也非原子指令,包含:

对象内存分配

初始化LazySingleton对象属性

将singleton引用指向内存空间

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

以上图的情况,线程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(){
        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单例等,敬请期待!(会有人期待吗 ::>_<:: )

参考资料

https://en.wikipedia.org/wiki...

https://www.cs.umd.edu/~pugh/...

https://www.jianshu.com/p/cf5...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77849.html

相关文章

  • 单例模式的终极实现方案

    摘要:如此便可使得这一实现方式能够同时具备线程安全延迟加载以及节省大量同步判断资源等优势,可以说是单例模式的最佳实现了 单例模式(Singleton)是一种使用率非常高的设计模式,其主要目的在于保证某一类在运行期间仅被创建一个实例,并为该实例提供了一个全局访问方法,通常命名为getInstance()方法。单例模式的本质简言之即是: 控制实例数目 以Java为例,单例模式通常可分为饿汉式和懒...

    Freelander 评论0 收藏0
  • vue-router 3.0版本中 router.push 不能刷新页面的问题

    摘要:分析原因实例后的不能刷新页面,应该是因为它与全局的中的的不是同一个,而之前的版本中能直接这样使用,应该是使用了单例。 在 github 的 vue-router 中找到同样的一个问题:3.0.1版本通过router实例无法跳转 昨天发现有些路由不能正常跳转,找了一下发现都是那些实例化后使用 router.push 而不是直接使用 this.$router.push 的地方。出现的情况是...

    xingqiba 评论0 收藏0
  • 再遇设计模式之JavaScript篇

    摘要:在面向对象的语言中,比如,等,单例模式通常是定义类时将构造函数设为,保证对象不能在外部被出来,同时给类定义一个静态的方法,用来获取或者创建这个唯一的实例。 万事开头难,作为正经历菜鸟赛季的前端player,已经忘记第一次告诉自己要写一些东西出来是多久以的事情了。。。如果,你也和我一样,那就像我一样,从现在开始,从看到这篇文章开始,打开电脑,敲下你的第一篇文章(或者任何形式的文字)吧。 ...

    Clect 评论0 收藏0
  • JS设计模式学习_基础篇

    摘要:工厂模式单例模式结构型设计模式关注于如何将类或对象组合成更大更复杂的结构,以简化设计。 一、写在前面 设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案 当然我们可以用一个通俗的说法:设计模式是解决某个特定场景下对某种问题的解决方案。因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式。 比如,当系统中某个接口的结构已经无法满足...

    venmos 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<