单例终极分析(一)

栏目: 后端 · 发布时间: 5年前

内容简介:如果你看过设计模式,肯定会知道这里简单提一下单例的用处。作为java程序员,你应该知道那么如何写一个单例呢?我想很多朋友都能搞定:

单例的用处

如果你看过设计模式,肯定会知道 单例模式 ,实际上这是我能默写出代码的第一个设计模式,虽然很长一段时间我并不清楚单例具体是做什么用的。

这里简单提一下单例的用处。作为 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-1 T-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() 也非原子指令,包含:

  1. 对象内存分配
  2. 初始化LazySingleton对象属性
  3. 将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单例 等,敬请期待!(会有人期待吗 ::>_<:: )

参考资料


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

JavaScript高级程序设计:第2版

JavaScript高级程序设计:第2版

Nicholas Zakas / 李松峰、曹力 / 人民邮电出版社 / 2010-7 / 89.00元

《JavaScript高级程序设计(第2版)》在上一版基础上进行了大幅度更新和修订,融入了近几年来JavaScript应用发展的最新成果,几乎涵盖了所有需要理解的重要概念和最新的JavaScript应用成果。从颇具深度的JavaScript语言基础到作用域(链),从引用类型到面向对象编程,从极其灵活的匿名函数到闭包的内部机制,从浏览器对象模型(BOM)、文档对象模型(DOM)到基于事件的Web脚本......一起来看看 《JavaScript高级程序设计:第2版》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具