为了让博客看起来不那么深入,我觉得可以让加入一点故事情节~ 锻炼一下以后写不动代码改写小说的能力~
最近准备找工作,这不今天就有家喊我去面试的;我一大早的就赶到了公司;
此处省略1万字跟面试官的客套话,直接进入正题;
面试官:你知道哪些设计模式阿?
我说:设计模式了解得不多,只知道单例模式跟工厂模式,装饰模式,适配器模式,享元模式,观察者模式;
面试官:哟,知道得还挺多的啊,行,先手写一个单例模式来看看;
自信的我迅速的在纸上写上了代码;还不忘加上注释,以体现出自己的代码规范;
//饿汉式
public class Singleton01 {
//1.将构造方法私有化;不允许外部直接创建对象;
private Singleton01(){
}
//2.利用static关键字创建类的唯一实例;依然用private修饰
private static Singleton01 singleton = new Singleton01();
//3.对外公开提供一个获取实例的方法,使用 public static修饰
public static Singleton01 getInstance(){
return singleton;
}
}
面试官一看,心想还行,不是个浑水摸鱼的,那试探一下,说:那你再写一个懒汉式给我看看;
我:心想这还不简单,饿汉懒汉不都是两个痴汉么?我又迅速的写完了懒汉式;
//懒汉式
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 关键字就可以了,于是快速的写下代码:
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 能解决并发引起的问题,但是每次访问该方法都需要获得锁,性能大大降低。当需要的对象被创建之后其实是不用再上锁了的。
还不等面试官发话,我马上又拿过一张纸,不就让锁的范围更小一点么,我双重检测判断是否有对象就好了嘛~
如果对象被创建,我就直接去拿,这个操作不需要加锁,创建的时候加锁不让其他线程去创建,这下应该行了:
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将会访问到一个还未初始化的对象。
一个未初始化的对象对于程序来说是没有多大意义的;这就跟一套不装修的房子一样,不能住人......
还不等面试官发话,于是我又匆匆的拿出纸笔写下了这段代码:
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 关键字可以保证 变量的 可见性 和有序性
以上故事情节纯属虚构,目的只是增加带入感,不知道有没有画蛇添足