设为首页 加入收藏

TOP

从JVM的角度来看单例模式(一)
2017-02-08 08:16:41 】 浏览:9441
Tags:JVM 角度 来看 单例 模式

最近在看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;


在执行这三句代码的时候,cpu可以先执行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个步骤:加载

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇JNI动态注册native方法及JNI数据.. 下一篇Java动态代理深入解析

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目