- 在面试的时候,我们常常会被问到这样一个问题:请您写出一个单例模式(Singleton Pattern)吧。好吧,写就写,这还不容易。顺手写一个:
/**
* 当对象很大时,没有使用这个对象之前就把他它加载到内存就会造成资源浪费(饿汉模式)
*/
public final class Cat {
private static Cat cat = new Cat();
private Cat(){}
public static Cat getCat() {
return cat;
}
}
- 这种写法就是所谓的 饥饿模式,每个对象在没有使用之前就已经初始化了。这就可能带来潜在的性能问题:如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。针对这种情况,我们可以对以上的代码进行改进,使用一种新的设计思想—— 延迟加载(Lazy-load Singleton)。
/**
* 改进
* 延迟加载对象也就是第一次用到的时候才去创建对象(懒汉模式)
*/
public final class Cat {
private static Cat cat = null;
private Cat(){}
//这样写有线程安全问题,比如说A线程进入的到if里面刚刚创建完对象,
// 这时线程B也进入到if里面,又创建新的对象,人家刚走你就来了,比隔壁老王还快
public static Cat getCat() {
if (cat == null) {
cat = new Cat();
}
return cat;
}
}
- 这种写法就是所谓的 懒汉模式。它使用了延迟加载来保证对象在没有使用之前,是不会进行初始化的。但是,通常这个时候面试官又会提问新的问题来刁难一下。他会问:这种写法线程安全吗?回答必然是:不安全。这是因为在多个线程可能同时运行到if里面,判断cat为null,于是同时进行了初始化。所以,这是面临的问题是如何使得这个代码线程安全?很简单,在那个方法前面加一个Synchronized就OK了。
/**
* 我们继续改进
* 延迟加载对象也就是第一次用到的时候才去创建对象(懒汉模式)
*/
public final class Cat {
private static Cat cat = null;
private Cat(){}
//同步的代价必然会一定程度的使程序的并发度降低。
//有没有什么方法,一方面是线程安全的,有可以有很高的并发度呢?
public static synchronized Cat getCat() {
if (cat == null) {
cat = new Cat();
}
return cat;
}
}
- 写到这里,面试官可能仍然会狡猾的看了你一眼,继续刁难到:这个写法有没有什么性能问题呢?答案肯定是有的!同步的代价必然会一定程度的使程序的并发度降低。那么有没有什么方法,一方面是线程安全的,有可以有很高的并发度呢?我们观察到,线程不安全的原因其实是在初始化对象的时候,所以,可以想办法把同步的粒度降低,只在初始化对象的时候进行同步。这里有必要提出一种新的设计思想—— 双重检查锁(Double-Checked Lock)。
/**
* 我们还得继续改进
* 延迟加载对象也就是第一次用到的时候才去创建对象(懒汉模式)
*/
public final class Cat {
private static Cat cat = null;
private Cat() {
}
//线程不安全的原因其实是在初始化对象的时候
//所以可以想办法把同步的粒度降低,只在初始化对象的时候进行同步。(细粒度锁)
public static Cat getCat() {
if (cat == null) {
synchronized (Cat.class) {
if (cat == null) {
cat = new Cat();
}
}
}
return cat;
}
}
-
这种写法使得只有在加载新的对象进行同步,在加载完了之后,其他线程在第一个if就可以判断跳过锁的的代价直接到return cat。做到了很好的并发度。
至此,上面的写法一方面实现了Lazy-Load,另一个方面也做到了并发度很好的线程安全,一切看上很完美。这时,面试官可能会对你的回答满意的点点头。但是,你此时提出说,其实这种写法还是有问题的!!问题在哪里?假设线程A执行到了第一个if,它判断对象为空,于是线程A执行到cat = new Cat(),去初始化这个对象,但初始化是需要耗费时间的,但是这个对象的地址其实已经存在了。此时线程B也执行到了第一个if,它判断不为空,于是直接跳到return cat得到了这个对象。但是,这个对象还 没有被完整的初始化!得到一个没有初始化完全的对象有什么用!!关于这个Double-Checked Lock的讨论有很多,目前公认这是一个Anti-Pattern,不推荐使用!所以当你的面试官听到你的这番答复,他会不会被Hold住呢? -
那么有没有什么更好的写法呢?有!这里又要提出一种新的模式—— Initialization on Demand Holder. 这种方法使用内部类来做到延迟加载对象,在初始化这个内部类的时候,JLS(Java Language Sepcification)会保证这个类的线程安全。这种写法最大的美在于,完全使用了Java虚拟机的机制进行同步保证,没有一个同步的关键字。
/**
* 我们还得继续继续改进
* 延迟加载对象也就是第一次用到的时候才去创建对象(懒汉模式)
*/
public final class Cat {
private static class CatHolder{
public final static Cat cat=new Cat();
}
//使用内部类来做到延迟加载对象,在初始化这个内部类的时候,
// JLS(Java Language Sepcification)会保证这个类的线程安全。
public static Cat getCat(){
return CatHolder.cat;
}
}
-
当一个类被JVM加载,这个类就会历经初始化的过程。由于该类没有任何静态变量的初始化,初始化就会顺利完成。而定义在类里面的静态类CatHolder直到JVM确定CatHolder一定会被执行时才会去初始化。也就是当静态方法getCat调用时,静态类CatHolder才会被执行,而当这件事第一次发生时,JVM才会去加载并初始化CatHolder。CatHolder 初始化时,就会初始化静态变量cat,就会执行外部类私有的构造器并赋值给cat。
由于类的初始化过程是串行的(JLS),就不会同时发生,也不需要同步操作。
并且,因为初始化阶段在串行操作里写入静态变量cat,所有接下来的并行调用cat方法会正确返回相同的cat,而不需要额外的同步开销。 -
这种线程安全的、无需synchronization的、且比无竞争的同步高效的单例模式,只能应用在构造函数保证不会失败的情况。
如果对你有帮助记得帮忙点个赞ღ( ´・ᴗ・` )比心
文献参考:
https://my.oschina.net/looly/blog/152865
https://www.cnblogs.com/fuyoucaoyu/p/6547715.html










网友评论