美文网首页
Java单例模式

Java单例模式

作者: 没有格子衬衫 | 来源:发表于2019-12-21 16:33 被阅读0次
  • 在面试的时候,我们常常会被问到这样一个问题:请您写出一个单例模式(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

相关文章

网友评论

      本文标题:Java单例模式

      本文链接:https://www.haomeiwen.com/subject/egugnctx.html