美文网首页
聊聊并发1:单例模式

聊聊并发1:单例模式

作者: SYFHEHE | 来源:发表于2018-04-23 00:01 被阅读0次

    0.单例模式用途

    单件模式属于工厂模式的特例,只是它不需要输入参数并且始终返回同一对象的引用。
    单件模式能够保证某一类型对象在系统中的唯一性,即某类在系统中只有一个实例。

    1.单例模式分类

    单例模式可以分为懒汉式和饿汉式
    饿汉式单例模式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。比如:

    No.1 饿汉式

    /*
     * 1.饿汉式
     *这种方式基于classloder机制避免了多线程的同步问题,
     *不过,instance在类装载时就实例化,
     *这时候初始化instance显然没有达到lazy loading的效果
     * 
     * */
    
    public class Singleton1 {
    
        private static Singleton1 instance = new Singleton1();// 直接初始化一个实例对象
    
        private Singleton1() {// private 类型的构造函数 抱着其他对象不能直接new一个该对象的实例
        }
    
        public static Singleton1 getInstance() {// 该类唯一的一个public方法
            return instance;
        }
    
    }
    

    那如何提升加载速度呢?我们就需要用到懒汉式的单例模式了。首先我们能想到的就是如下所示的这种做法,首先 不初始化一个实例对象,等拿到是空时再去初始化,但是它有一个问题:在多线程不能正常工作,原因就是:判断是不是为空和实例化对象不是原子操作

    No.2 懒汉,线程不安全

    /*
     * 懒汉模式,线程不安全
     * 
     * */
    public class Singleton2 {
        
        private static Singleton2 instance;
        
        private Singleton2(){}
        
        public static Singleton2 getInstance(){
            if(instance == null){
                instance = new Singleton2();
            }
            return instance;
        } 
    
    }
    

    懒汉模式在使用时,容易引起不同步问题,所以我们很自然的想到用synchronized关键字创建同步"锁"如下:

    No.3 懒汉,线程安全

    /*
     * 懒汉,线程安全
     * 
     * */
    public class Singleton3 {
        
        private static Singleton3 instance;
        
        private Singleton3(){}
        
        public static synchronized Singleton3  getInstance(){
            if(instance == null){
                instance = new Singleton3();
            }
            return instance;
        } 
    
    }
    

    这种做法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是效率很低(因为锁),因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这个时候我们可以用双重同步锁来解决这个问题。它用同步块加锁的方式对代码块进行加锁操作。

    No.4 双重校验锁

    /*
     * 双重校验锁
     * 
     * */
    public class Singleton4 {
    
        private volatile static Singleton4 instance;
    
        private Singleton4() {
        }
    
        public static Singleton4 getInstance() {
            if (instance == null) {
                synchronized (Singleton4.class) {
                    if (instance == null) {
                        instance = new Singleton4();
                    }
                }
            }
            return instance;
        }
    
    }
    
    

    有人会问:为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
    注意:这里的 instance 变量是被声明成 volatile 。为什么?
    因为instance = new Singleton4()并不是原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情,这个在我以前的文章里也说了:

    • 1.首先是会给 instance 分配内存
    • 2.调用 Singleton 的构造函数来初始化成员变量
    • 3.将instance对象指向分配的内存空间。

    但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
    那怎么怎么应对,这里我们只需要将 instance 变量声明成 volatile 就可以了。
    原因是:volatile关键字除了可以使得变量在不同线程间具有可见性,而且可以阻止JVM将指令重排序。

    那还有没有一种方法既不用加锁,也能实现懒加载。用静态内部类。

    No.5 静态内部类

    /*
     * 静态内部类
     * 
     * 既不用加锁,也能实现懒加载。
     * 
     * */
    public class Singleton5 {
        
        private Singleton5(){}
        
        private static class SingletonHolder{
            private static final Singleton5 instance = new Singleton5();
        }
        
        private static Singleton5 getInstance(){
            return SingletonHolder.instance;
        }
    
    }
    
    

    这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种方式不同的是:第三种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三方法就显得更合理。

    相关文章

      网友评论

          本文标题:聊聊并发1:单例模式

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