前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一次单例模式引发的深思

一次单例模式引发的深思

作者头像
用户2141593
发布2019-02-20 14:58:10
5270
发布2019-02-20 14:58:10
举报
文章被收录于专栏:Java进阶Java进阶

为了让博客看起来不那么深入,我觉得可以让加入一点故事情节~ 锻炼一下以后写不动代码改写小说的能力~

最近准备找工作,这不今天就有家喊我去面试的;我一大早的就赶到了公司;

此处省略1万字跟面试官的客套话,直接进入正题;

面试官:你知道哪些设计模式阿?

我说:设计模式了解得不多,只知道单例模式跟工厂模式,装饰模式,适配器模式,享元模式,观察者模式;

面试官:哟,知道得还挺多的啊,行,先手写一个单例模式来看看;

自信的我迅速的在纸上写上了代码;还不忘加上注释,以体现出自己的代码规范;

代码语言:javascript
复制
	//饿汉式
public class Singleton01 {
	
	//1.将构造方法私有化;不允许外部直接创建对象;
	private Singleton01(){
	}
	
	//2.利用static关键字创建类的唯一实例;依然用private修饰
	private static Singleton01 singleton = new  Singleton01();
	
	//3.对外公开提供一个获取实例的方法,使用 public static修饰
	public static Singleton01 getInstance(){
			return singleton;
	}
	
}

面试官一看,心想还行,不是个浑水摸鱼的,那试探一下,说:那你再写一个懒汉式给我看看;

我:心想这还不简单,饿汉懒汉不都是两个痴汉么?我又迅速的写完了懒汉式;

代码语言:javascript
复制
	//懒汉式
public class Singleton02 {

	//1.将构造方法私有化,还是不允许外部直接用new的方式产生对象
	private Singleton02(){
		
	}
	
	//2.还是使用static关键字,不过这次只是声明一个类的唯一实例而已,我们不急着创建对象~
	private static Singleton02 singleton02;
	
	//3.对外公开提供一个获取实例的方法,使用 public static修饰
	public static Singleton02 getInstance(){
		if(singleton02==null){
			singleton02 = new Singleton02();
		}
		return singleton02;
	}
}

面试官:还不错,代码也挺规范的;

我:此时还挺陶醉的,心想面试也太容易了吧。哈哈~

还没容我乐够三秒,面试官又发话了;

面试官:你看看你写的饿汉式的单例模式,能说说这段代码在什么情况下会出现bug么?

我:还沉醉其中的我,突然慌了...  面试前背的单例模式都是网上找的模板阿,怎么会有bug呢? 我去,我哪知道有什么bug啊。。。 该死的百度,太不靠谱了,此时的我,也没太多的心情去黑百度了;

只能硬着头皮看着自己写的代码,首先私有化构造方法,不让外部直接调用这肯定是没错的;

第二步,使用static关键字保证Singleton02对象在 Singleton02这个类被加载一次已确保会是单例;

第三步,对外提供能够获取实例的方法,先判断Singleton02这个对象存不存在,不存在就创建Singleton02对象,存在就不创建,提升程序运行速度,这也没问题啊,返回值,修饰符都没问题,问题在哪呢? 作为菜鸟的我此时已瑟瑟发抖; 心想面试官不是诈我的吧。

我说:面试官,你好,我检查了一下,貌似没有问题;

面试官:我想,这大概就是你们初级程序员不够稳的地方吧。你仔细看看你懒汉式的第三步的判断,在多线程的情况下;会发生意外!

说个最简单的例子,如果把singleton02比作是一个妹子。你这段代码定的规矩是:如果该妹子为单身,你只要买套房子( new Singleton02() ),然后把这套房子写上妹子的名字,这个妹子就是你女朋友了; 但是,现实情况远远没有你想的那么简单! 你想要这个妹子,别人也想要这个妹子呢!

你看到这个妹子是单身,于是你就去买了一套房子,正准备开开心心的在房产证写上妹子的名字,期待佳人的时候,突然发现妹子已经名花有主了,什么情况?A同学在你看到妹子是单身的时候,他也想追妹子,他跟你一起去小区看的房子,一起付的款,但是A比你先去找房地产公司,先你一步在房产证上写了妹子的名字,妹子是A的了,当然你还是可以写妹子的名字,这样妹子就有两套房子了,但是你女朋友就跟别人跑了。。

此刻的我,恍然大悟,单例模式的初衷是  保证在整个应用程序中某个实例对象有且只会有一个。写饿汉式的时候面试官没有找我的茬是因为第二步对象的创建加了static关键字,在类加载的时候就已经是   加载且只加载一次 ; 所以不会出现线程安全的问题,而懒汉式,第二步只是声明了一个对象而已,并没有创建,创建对象的时候又没有加锁进行同步,也就意味着所有线程只要通过了 if 的那个判断就会去创建对象,如果A线程进入了 if  的判断, 只要new Singleton02()对象没有跟 singleton02 发生引用(看理解为 句柄 或 “指针”关系),那么 singleton02 就还是为null,  此时如果又有B线程进来了,他也会 去new Singleton02()然后赋值给singleton02,此时,就算A线程先进来,女朋友也没了,谁让B线程动作更快呢!

此时的我后悔不已,这么简单的问题怎么就没看出来呢......

面试官看我似乎是因为紧张了没看出来这个问题,于是问我:现在既然你知道会出现线程安全的问题,那么改怎么解决呢?

筐瓢的我,此时已经不能失误了,仔细回忆起脑袋里关系线程安全的知识,加锁,对加锁可以保证线程安全,怎么加?加在哪儿?

短暂的思考,给方法加上synchronized 关键字就可以了,于是快速的写下代码:

代码语言:javascript
复制
public class Singleton {
    private Singleton() {    }
    private static Object INSTANCE = null;
    public synchronized static Object getInstance() {
        if(INSTANCE == null){
            INSTANCE = new Object();
        }
        return INSTANCE;
    }
}

面试官问并没有直接的问我代码的问题;

面试官:你能说说 Hashmap、Hashtable跟 ConcurrentHashmap 有什么区别吗?

我:Hashmap 不是线程安全的,Hashtable、 ConcurrentHashmap 是线程安全的。

面试官:那你再想想你写的这个加锁的懒汉单例有什么问题。

此时的我马上反应过来Hashmap 不是线程安全的是因为put操作没有加 锁,Hashtable 跟 ConcurrentHashmap 的差别是

Hashtable 的锁是锁住了整个 数组(桶),ConcurrentHashmap 使用的是分段锁,锁的只单个桶;面试官这是在按时我,

使用synchronized 关键字锁的范围太大了,颗粒度应该小一点,虽然synchronized 能解决并发引起的问题,但是每次访问该方法都需要获得锁,性能大大降低。当需要的对象被创建之后其实是不用再上锁了的。

还不等面试官发话,我马上又拿过一张纸,不就让锁的范围更小一点么,我双重检测判断是否有对象就好了嘛~

如果对象被创建,我就直接去拿,这个操作不需要加锁,创建的时候加锁不让其他线程去创建,这下应该行了:

代码语言:javascript
复制
public class Singleton {
    private Singleton() {    }
    private static Object INSTANCE = null;
    public static Object getInstance() {
        if(INSTANCE == null){
            synchronized(Singleton3.class){
                if(INSTANCE == null){
                    INSTANCE = new Object();
                }
            }
        }
        return INSTANCE;
    }
}

面试官:本来你写的代码只是丢失点性能,你这样写是可能引起错误的!

INSTANCE = new Object(); 你知道这行代码是什么意思吗?

我:创建一个对象啊。

面试官:没那么简单,JVM会把 INSTANCE = new Object(); 拆分成三个动作;

1 、首先会给 new Object() 分配一个内存空间;

2 、 然后初始化 new Object()这个对象;

3 、INSTANCE 指向  new Object()的内存地址;

重排序:

这 1、2、3 三步并不是一个原子操作!你看起来是 1 > 2 >3 顺序执行,但是JVM 执行你的代码时会对你的代码的执行效率进行重新的排序,目的是提高程序的运行效率,但这仅仅适用于单线程的情况下;

1 是第一步这是必须的, 但是如果是 1 > 3 > 2  你的代码又有问题了;

JVM 的内存模型 关于指令的重排序我还是懂的,Java内存模型   万万没想到不起眼的INSTANCE = new Object()还有这个奥秘...

从图中可以看出A2和A3的重排序,将导致线程 B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。

一个未初始化的对象对于程序来说是没有多大意义的;这就跟一套不装修的房子一样,不能住人......

还不等面试官发话,于是我又匆匆的拿出纸笔写下了这段代码:

代码语言:javascript
复制
public class Singleton {
    private Singleton() {}
    private static volatile Object INSTANCE = null;
    public static Object getInstance() {
        if(INSTANCE == null){
            synchronized(Singleton.class){
                if(INSTANCE == null){
                    INSTANCE = new Object();
                }
            }
        }
        return INSTANCE;
    }
}

还未等面试官发话,我就主动向面试官解释 我这段代码了:

volatile关键字能保证变量的 可见性和有序性(禁止重排序);volatile 关键字解析 volatile 和 synchronized  一起使用;第一次检测的时候是不加锁的,这样不会影响代码的效率,第二次检测的时候加锁保证不会创建多个对象,并且给 变量加上了 

volatile关键字 这样 变量 引用 对象的过程 的顺序是固定的,不会引起其他线程的读操作出问题;

做个小总结:

1 、 synchronized 关键字可以保证 操作的原子性 和 可见性 

2 、volatile 关键字可以保证 变量的 可见性 和有序性 

以上故事情节纯属虚构,目的只是增加带入感,不知道有没有画蛇添足

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017年06月05日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档