最近在看jvm,发现随着自己对jvm底层的了解,现在对java代码可以说是有了全新的认识。今天就从jvm的角度来看一看以前自以为很了解的单例模式。

了解单例模式的人都知道,单例模式有两种:“饿汉模式”和“懒汉模式”。

引用一段网上对这两种模式的介绍:

“饿汉模式的特点是加载类时比较慢,但运行时获取对象的速度比较快,线程安全。饿汉式是线程安全的,在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变。懒汉模式的特点是加载类时比较快,但是在运行时获取对象的速度比较慢,线程不安全, 懒汉式如果在创建实例对象时不加上synchronized则会导致对象的访问不是线程安全的。所以在此推荐大家使用饿汉模式。”

笔者先给出结论“上面这段描述可以说是完全不正确,最后给出的结论还算勉强正确,为什么说勉强正确,因为我不会推荐大家使用饿汉模式,我会直接说就用饿汉模式,懒汉模式在任何情况下都不需要”。

网上这段文字的错误主要有两点

  1. 懒汉模式线程不安全,如果想线程安全必须加synchronized
  2. 饿汉模式在加载类时会慢

先来看一下懒汉模式,不用synchronized也能实现线程安全

先来回顾一下懒汉模式的“发展史”

懒汉模式V1.0:

package common;

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

懒汉模式V1.0看起来就很不安全,当同时有两个线程调用 getInstance()方法时,很容易让两个线程都进入if块导致new 了两次对象。

于是在某一次大会上,有砖家发布了下面这种叫做DCL(double check lock)的错误写法,因为是砖家发布的,因此这种错误写法在网上广为流传,我在公司也看到有人这么写,这种我们可以称为懒汉模式V2.0

package common;

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

懒汉模式V2.0解决了1.0中可能会new两次对象的问题,但是依然有问题。

这里我们先引入一个概念——指令重排序:编译器或处理器为了优化程序性能而采取的对指令进行重新排序执行的一种手段。

比如:

int a=1;

int b=a+1;

int c=2;

在执行这三句代码的时候,在编译器和处理器对程序进行优化之后,可以先执行int c=2,再执行另外两句,这就是指令重排序。

但是很显然,指令重排序并不是可以随便乱排的,比如int b=a+1这句依赖了a的值,因此必须要在int a=1之后执行才能保证最终b的值是正确的。因此,指令重排序后,要保证在单个线程里,执行结果和重排序前是等效的。

这里为什么强调是单个线程呢?比如刚刚的例子,假如abc都是全局变量,我们把c=2这一句重排序到第一句,从执行这三句代码的线程的角度,执行完三句代码后abc的值和重排序之前是一致的。

但是假设现在有另外一个线程在不停的打印abc的值,那么因为重排序的关系,在打印结果里就会出现c=2而ab还没有被赋值的结果。因此,在指令重排序后,从重排序的这个线程自身来看,重排序后的代码可以看作是有序的(因为保证运行结果不变),而从其他线程的角度来看,重排序后的代码是乱序执行的。

回到我们的懒汉模式V2.0,我们现在知道了,当多线程并发的时候,假如第一个线程成功获取锁并进入if块执行singleton=new Singleton(),

这句代码我们可以看成三步操作:

  1. 在堆内存中划分一个Singleton对象实体的空间
  2. 初始化堆内存中对象实例的数据(字段等)
  3. 将singleton变量通过指针指向生成的对象实体

这个时候因为指令重排序,可能在步骤2还没有执行完的时候,步骤3已经执行完了,

这时候singleton变量已经不为null,此时如果有并发的线程执行getInstance()方法,将获取到一个没有初始化完成的Singleton对象从而引发错误。

为了解决这个问题,我们给singleton变量添加关键字volatile得到懒汉模式V3.0:

package common;

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

这里用volatile修饰singleton并不是用了volatile的可见性,而是用了java内存模型的“先行发生”(happens-before)原则的其中一条:

Volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”指时间上的先后顺序。

这样一来就能禁止指令重排序,确保singleton对象是在初始化完成后才能被读到。

懒汉模式V3.0可以说是懒汉模式的终极形式,经过2次修改终于线程安全了,然而并没有什么卵用,因为饿汉模式先天就没有线程安全问题,而且也并不像网上说的那样,上来就要创建实例。

饿汉模式解析:

网上一般的说法是,饿汉模式会导致程序启动慢,因为一上来就要创建实例。相信这么说的人一定是不了解java的类加载机制。先上个饿汉模式的代码:

package common;

public class Singleton {
private static final Singleton singleton=new Singleton(); public static Singleton getInstance(){
return singleton;
}
}

可以看到new实例是直接写在了静态变量后面,还有一种写法:

package common;

public class Singleton {
private static final Singleton singleton; static{
singleton=new Singleton();
} public static Singleton getInstance(){
return singleton;
}
}

这两种写法在编译后是完全等效的,

类的加载分为5个步骤:加载、验证、准备、解析、初始化

初始化就是执行编译后的<cinit>()方法,而<cinit>()方法就是在编译时将静态变量赋值和静态块合并到一起生成的。

所以说,“饿汉模式”的创建对象是在类加载的初始化阶段进行的,那么类加载的初始化阶段在什么时候进行呢?jvm规范规定有且只有以下7种情况下会进行类加载的初始化阶段:

  1. 使用new关键字实例化对象的时候
  2. 设置或读取一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候
  3. 调用一个类的静态方法的时候
  4. 使用java.lang.reflect包的方法对类进行反射调用的时候
  5. 初始化一个类的子类(会首先初始化父类)
  6. 当虚拟机启动的时候,初始化包含main方法的主类
  7. 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

基本来说就是只有当你以某种方式调用了这个类的时候,它才会进行初始化,而不是说jvm启动的时候就初始化,所以说假如你的单例类里只有一个getInstance()方法,那基本上就是当你从其他类调用getInstance()方法的时候才会进行初始化,这事实上和“懒汉模式”是一样的效果。而jvm本身会确保类的初始化只执行一次。

当然,也有一种可能就是单例类里除了getInstance()方法还有一些其他静态方法,这样当调用其他静态方法的时候,也会初始化实例,但是这个很容易解决,只要加个内部类就行了(这种模式叫holder pattern):

package common;

public class Singleton {
private static class SingletonHolder{
private static Singleton instance=new Singleton();
} public static Singleton getInstance(){
return SingletonHolder.instance;
}
}

这样只有当调用getInstance()方法的时候,才会初始化内部类SingletonHolder。

总结

经过以上分析,“懒汉模式”实现复杂而且没有任何独占优点,“饿汉模式”完胜。

从jvm的角度来看单例模式的更多相关文章

  1. 从jvm的角度来看java的多线程

    最近在学习jvm,发现随着对虚拟机底层的了解,对java的多线程也有了全新的认识,原来一个小小的synchronized关键字里别有洞天.决定把自己关于java多线程的所学整理成一篇文章,从最基础的为 ...

  2. 从JVM的角度解析String

    1. 字符串生成过程 我们都知道String s = "hello java";会将“hello java”放入字符串常量池,但是从jvm的角度来看字符串和三个常量池有关,clas ...

  3. 从JVM的角度看JAVA代码--代码优化

    从JVM的角度看JAVA代码–代码优化 从JVM的角度看JAVA代码代码优化 片段一反复计算 片段二反复比較 在JVM载入优化为class文件,运行class文件时,会有JIT(Just-In-Tim ...

  4. 以代码爱好者角度来看AMD与CMD

    随着浏览器功能越来越完善,前端已经不仅仅是切图做网站,前端在某些方面已经媲美桌面应用.越来越庞大的前端项目,越来越复杂的代码,前端开发者们对于模块化的需求空前强烈.后来node出现了,跟随node出现 ...

  5. 从字节码和JVM的角度解析Java核心类String的不可变特性

    1. 前言 最近看到几个有趣的关于Java核心类String的问题. String类是如何实现其不可变的特性的,设计成不可变的好处在哪里. 为什么不推荐使用+号的方式去形成新的字符串,推荐使用Stri ...

  6. 以代码爱好者角度来看AMD与CMD(转)

    随着浏览器功能越来越完善,前端已经不仅仅是切图做网站,前端在某些方面已经媲美桌面应用.越来越庞大的前端项目,越来越复杂的代码,前端开发者们对于模块化的需求空前强烈.后来node出现了,跟随node出现 ...

  7. 从源码角度来看BeanFactory和ApplicationContext的关系

    大家好,我是小黑,这是年前的最后一篇推文,提前祝大家新年快乐~~ 这次我们从源码角度来聊聊BeanFactory和ApplicationContext的关系,讲一些网上文章不曾提到的点. 官方描述 先 ...

  8. jvm005 从jvm的角度谈谈线程的实现

    一.线程的实现 在谈谈线程之前,我们要先知道线程是何物?在学习操作系统时,我们得知进程和线程的概念,接下来我们将开始揭示线程. 什么是进程?通过任务管理器我们就看到了进程的存在.而通过观察,我们发现只 ...

  9. 从数学分析的角度来看Softmax

    作者:无影随想 时间:2016年1月. 出处:https://zhaokv.com/machine_learning/2016/01/softmax-calculous-perspective.htm ...

随机推荐

  1. SQL复习五(索引)

    SQL索引在数据库优化中占有一个非常大的比例, 一个好的索引的设计,可以让你的效率提高几十甚至几百倍,在这里将带你一步步揭开他的神秘面纱. 1.1 什么是索引? SQL索引有两种,聚集索引和非聚集索引 ...

  2. windows服务器下IIS7 安装URL Rewrite(URL重写)模块

    URL Rewrite Module是一个基于规则的URL重写引擎,用于在URL被Web服务器处理之前改变请求的URL.对于动态Web应用程序,它可以为用户和seo/seo.html" ta ...

  3. Java 内存回收机制 -说到点上了

    下面这个图,很清楚地说明对象在new的时候是怎样开辟内存空间的 其中对象new出来的,是栈内存,变量的开辟是堆内存 Java的一个重要优点就是通过垃圾收集器GC (Garbage Collection ...

  4. Shell条件与测试

    分类参考 文件状态测试 -b filename 当filename 存在并且是块文件时返回真(返回0) -c filename 当filename 存在并且是字符文件时返回真 -d pathname ...

  5. MFC中PeekMessage的使用,非阻塞消息循环

    在程序设计的时候经常要进行一个数据循环,比如播放音乐需要循环的向缓冲区里面写入数据,在这个时候比较通用的方法是建立一个线程做事情,但是有时候不想创建多线程就可以使用微软提供的PeekMessage方法 ...

  6. realm-java 源码疑问

    JNIEXPORT void JNICALL Java_io_realm_internal_Group_nativeWriteToFile( JNIEnv* env, jobject, jlong n ...

  7. 苹果App Store开发者帐户从申请,验证,到发布应用(1)

    app store为开发者提供四种类型的申请: 个人ios开发者计划$99/年 公司ios开发者计划$99/年 企业ios开发者计划$299/年 高校ios开发者计划免费 在这里主要介绍一下公司ios ...

  8. javascript--study

    1.函数传参:按值传递 对于数字.字符串等是将它们的值传递给了函数参数,函数参数的改变不会影响函数外部的变量. 对于数组和对象等是将对象(数组)的变量的值传递给了函数参数,这个变量保存的指向对象(数组 ...

  9. UVa 793 - Network Connections

    题目大意:给出计算机之间的连接配置,询问某两台计算机是否相连.判断两个点是否在同一个连通分量里,用并查集处理. #include <cstdio> #define MAXN 1000000 ...

  10. Swing 窗口的最小化到系统图标与还原

    2014年2月26日 13:01:47 一个上午的功夫,终于折腾好了. 上午主要是卡在监听事件的参数问题.当时脑子不好使,忘记事件是自己构造的,傻傻的测试了半天,如何传递窗口的参数 等中午解决的时候, ...