有多少种单例模式,枚举算不算单例,单例模式中不用volatile会导致什么问题?volatile特性是什么?为什么android中不推荐使用枚举。

有多少种单例模式 

以公司中的CEO来举例,一个公司中只能有一个CEO,所以CEO就是单例,可以使用单例模式来实现,下面以这个例子来实现单例.

1、饿汉方式

public class CEO{
    //静态对象
    private static CEO mCeo = new CEO();
    //构造函数私有
    private CEO(){}
    
    //公有的静态函数,对外暴露获取单例对象的接口
    public static CEO getInstance(){
        return mCeo;
    }
}

这种方式叫做饿汉方式,它的关键点是:

1、构造函数私有;

2、通过一个静态方法返回一个静态对象实例.

为什么这种方式能够保证实例的唯一性呢?因为在同一个类加载器下,类的初始化只会进行一次,并且在多线程环境下,JVM会保证只有一个线程执行类的初始化,所以当我们第一次调用getInstance方法,访问mCeo静态变量时,CEO类还没有没有进行初始化,就会首先进行CEO类的初始化,类的初始化之前会经历加载、验证、准备、解析阶段,然后才到初始化阶段,在初始化阶段中JVM会执行static语句块,此时就会为CEO在java堆上为分配一块内存,然后把mCeo指向这块内存,即实例化了CEO,接着getInstance方法就会返回CEO的实例,当我们第二次调用getInstance()方法时,它会返回上次在类初始化阶段创建的CEO实例,而不会再进行一次类的初始化,所以这就保证了每次调用getInstance()方法都是返回同一个实例

饿汉方式

优点是线程安全,类初始化时就完成实例的创建,以后调用getInstance方法获取对象实例时速度比较快

缺点是会造成类初始化过程变慢,还可能会提前初始化单例类,例如CEO中有另外一个静态方法hello(),我第一次调用时并不是调用getInstance方法而是调用hello方法,它也会进行CEO类的初始化,导致单例类也完成实例创建,但此时我并没有使用这个单例类,所以说饿汉方式的优点也是它的缺点。

类的初始化和类的实例化是两个不同的过程,在类的实例化过程中,如果发现类还未进行初始化,就会先进行类的初始化.

2、静态内部类形式

public class CEO{
    //构造函数私有
    private CEO(){}
    
    public static CEO getInstanse(){
        //返回静态内部类中的静态变量实例
        return SingletonHolder.mCeo;
    }

    /**
     * 静态内部类
     */
    private static class SingletonHolder{
        private static CEO mCeo = new CEO();
    }
    
}

静态内部类方式的关键点是:

1、构造函数私有;

2、通过一个静态方法返回静态内部类中的静态变量实例.

静态内部类形式的特点和饿汉方式一样都是基于类的初始化保证实例的唯一性,同时它在饿汉方式的基础上增加了延迟初始化,在饿汉方式中说到在CEO类初始化时,就会完成单例类实例的创建,这样就可能导致了提前初始化了单例类,造成资源浪费,而静态内部类就解决了这个缺点,当第一次初始化CEO类时并不会创建CEO实例,只有在调用getInstance方法时才会导致CEO类实例化,因为第一次调用getInstance方法时会让JVM初始化SingletonHolder类,在初始化SingletonHolder类的同时完成了CEO单例类实例的创建,然后返回CEO实例,以后调用getInstance方法时都会返回同一实例.

优点是不仅能保证线程安全,也能保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式

缺点就是第一次加载时反应稍慢。

为什么基于类初始化的单例就是线程安全的呢?这是因为类的初始化过程其实是在执行clinit方法,clinit方法又叫做类构造器,clinit方法是由编译器收集所有的静态变量赋值动作和static语句块合并形成的,在多线程环境下,JVM执行clinit方法时,会给clinit方法加锁,多个线程初始化类时,只有一个线程会获得clinit方法的执行权,其他线程会阻塞等待,等某个线程执行完clinit方法后,就完成了类的初始化,这时就会唤醒其他等待线程,其他等待线程发现类已经执行过clinit方法了,就不再重复执行了,所以这就是单例模式线程安全实现的保证,也是单例模式实例唯一性的保证。

3、懒汉模式(线程安全)

public class CEO {
    private static CEO mCeo;
    //构造函数私有
    private CEO(){}
    
    //方法加上synchronized关键字
    public static synchronized CEO getInstance(){
        //判空处理,保证只实例化一次
        if(mCeo == null){
            mCeo = new CEO();
        }
        return mCeo;
    }
}

这种方式叫做线程安全的懒汉模式,它的关键点是:

1、构造函数私有;

2、通过一个同步静态方法返回静态对象实例;

3、静态对象实例创建时加入了判空处理,保证只实例化一次.

通过给 getInstance方法加synchronized关键字,保证了在多线程环境下只有一个线程对CEO进行初始化,同时又加入了判空处理,避免了重复创建对象实例,保证每次调用getInstance方法都会返回同一个实例.

懒汉模式

优点是线程安全,单例只有在使用时才会被实例化,节约资源,即延迟初始化

缺点是第一次加载时需要及时进行实例化,反应稍慢,还有每次调用getInstance方法时都进行同步,造成不必要的同步开销这种模式不建议使用

还有一种叫非线程安全的懒汉模式,与线程安全的懒汉模式相比,只是少了一个synchronized关键字,不适合在多线程环境下使用,因为没有正确同步会造成创建多个实例,适合在单线程环境下使用。

4、Double Check Lock(DCL)

public class CEO {
    private static CEO mCeo = null;
     //构造函数私有
    private CEO(){}
    
    public static CEO getInstance(){
        //第一次判空,保证只同步一次
        if(mCeo == null){//1
            //synchronized语句块,锁住的是类的Class对象
            synchronized (CEO.class){
                //第二次判空,保证只实例化一次
                if(mCeo == null){
                    mCeo = new CEO();//2
                }
            }
        }
        return mCeo;
    }
}

这种方式的名字叫双重检查锁定,简称DCL,它的关键点是:

1、构造函数私有;

2、通过一个静态方法返回静态对象实例;

3、静态对象实例创建时加入了双重判空处理 + synchronized块,保证只同步一次只实例化一次.

在getInstance方法中对实例进行了俩次判空:第一次判空是为了避免不必要的同步,解决了懒汉模式每次调用getInstance方法都需要同步的缺点,只有对象为null的情况下才进入synchronized块,才需要同步;第二次判空则是为了进入synchronized块后只有对象为null的情况下才创建实例,避免重复创建对象实例,而且synchronized块锁住的是类的Class对象,保证了在多线程环境下只有一个线程进入synchronized块,所以采用DCL方式每次调用 getInstance方法返回的都是同一个实例

DCL

优点是资源利用率高,第一次执行getInstance方法时单例对象才会被实例化,即延迟初始化,第一次实例化时才进行同步,减少了同步开销,并且能在大多数情况下保证单例唯一性;

缺点是第一次加载反应稍慢,因为又要加锁,又要初始化对象,导致第一次调用getInstance方法返回较慢。


为什么说DCL在大多数情况能下保证单例唯一性?这说明在少数情况下DCL还是会出现问题的,问题就出现在注释2:mCeo = new CEO(); 在CEO第一次进行实例化过程中,这个实例化过程可以分为以下3步:

1、在java堆分配中CEO对象的内存空间;

2、进行CEO类的初始化过程;

3、把mCeo指向1步骤中分配的内存空间.

某些JVM会把这3个步骤进行指令重排序,变为以下顺序:

1、在java堆分配中CEO对象的内存空间;

3、把mCeo指向1步骤中分配的内存空间;

2、进行CEO类的初始化过程.

单线程环境下这样是没有问题的,因为就算指令重排,在getInstance方法返回时mCeo指向的对象已经完成了初始化

多线程环境下就出现问题了,假设现在有两个线程A、B,线程A执行到了getInstance方法的注释2,即进行CEO的实例化,由于指令重排,线程A先执行1、3步骤,此时mCeo已经指向了分配的内存空间,导致此时的mCeo != null,而恰好线程B此时执行到了getInstance方法的注释1,进入判断 if(mCeo == null),因为此时mCeo != null,所以条件判断为false,不进入if语句,直接来到return语句,返回了还没初始化完毕的mCeo ,这样就可能导致程序崩溃!因为你在使用一个还未初始化完成的对象


针对DCL的错误,有两种解决办法,第一种办法是使用Volatile关键字,因为Volatile会禁止指令重排序,保证对象实例化过程按1、2、3步骤进行;第二种办法是再加一个局部变量做一层缓冲,下面分别使用来完善DCL:

解决方法1:使用Volatile关键字

public class CEO {
    //加上Volatile关键字修饰
    private volatile static CEO mCeo = null;
    private CEO(){}
    
    public static CEO getInstance(){
        if(mCeo == null){
            synchronized (CEO.class){
                if(mCeo == null){
                    mCeo = new CEO();
                }
            }
        }
        return mCeo;
    }
}

熟悉Volatile特性的朋友都知道,在多线程环境下,Volatile会禁止指令重排序保证内存可见性,所以线程执行到mCeo = new CEO()时,保证CEO类初始化完毕后才把mCeo引用指向java堆的内存空间,避免另外一个线程访问到未初始化完毕的mCeo。

解决方法2:增加一个局部变量

public class CEO {
    private static CEO mCeo = null;
    private CEO(){}
    
    public static CEO getInstance(){
        //1、增加一个局部变量,同为CEO类型
        CEO ceo = null;
        if(mCeo == null){
            synchronized (CEO.class){
                if(mCeo == null){
                    //2、执行实例时,先实例化这个局部变量
                    ceo = new CEO();
                    //3、待局部变量实例化完毕后,才把这个实例赋值给要返回的静态变量mCeo
                    mCeo = ceo;
                }
            }
        }
        return mCeo;
    }
}

这种方法也能够保证DCL的正确性,因为它是先把同为CEO类型的局部变量ceo实例化后,才赋值给mCeo,这就不管ceo实例化过程中怎么样重排序,在ceo还未初始化完毕之前,mCeo一直为null,当ceo实例化完毕后,mCeo才指向它,这样就避免了mCeo指向一个未初始化完毕的对象。

使用DCL时,建议使用解决方法1、2中的DCL方式。

5、枚举模式

public enum CEO{
    INSTANCE;
    
    //枚举中还可以定义一些方法和字段
    String name = "ceo";
    public void doSomething(){
        System.out.println("do Something");
    }
}

枚举模式的关键点是:在枚举类中定义一个枚举,叫什么名字都可以,这里叫INSTANCE,而且只能定义一个枚举,不能定义第二个枚举如INSTANCE2。

写法简单是枚举单例最大的优点,枚举实例的创建天生就是线程安全的,并且任何情况下它都是一个单例,我们直接通过CEO.INSTANCE就可以访问到这个单例,枚举中还可以为这个单例定义一些方法,例如这里我定义了一个doSomething方法,我通过**CEO.INSTANCE.doSomething()**就可以调用这个方法,字段的访问同理。

那么枚举的实现原理是什么?接下来把CEO.java文件编译成CEO.class,然后通过jad工具反编译,如下:

//CEO枚举类反编译后的java代码
public final class CEO extends Enum{
    //...
    
    private CEO(String s, int i){
        super(s, i);
        name = "ceo";
    }
    
   public static final CEO INSTANCE;

    String name;
    public void doSomething(){
        System.out.println("do Something");
    }

    static {
        INSTANCE = new CEO("INSTANCE", 0);
        //...
    }
}

我省略了一些无关代码,可以看到CEO继承自一个Enum类,所以枚举类本质还是一个类,并且它是final的,所以它不可被继承,它里面的构造方法私有,并且INSTANCE字段是一个静态变量,在static语句块中实例化,所以枚举模式保持单例唯一性的本质还是基于类的初始化,它的原理和前面讲过的饿汉方式、静态内部类形式一样。

在effective java中,枚举模式被推荐为实现的单例是最好的方式

6、使用容器实现

前面所讲的方式都是针对单个类的单例,如果一个程序中存在着多种类型的单例,就可以通过一个容器把它们集中管理:

public class SingletonManager extends Staff {
    private static Map<String, Object> mServices = new HashMap<>();
    
    private SingletonManager(){}
    
     public static void registerService(String key, Object instance){
         //加入了一个判断处理,避免重复添加
        if(!mServices.containsKey(key)){
            mServices.put(key, instance);
        }
    }
    
    public static Object getService(String key){
        return mServices.get(key);
  }

这种方式的关键点是:将多种类型的单例注入到一个统一的管理类中,且只能注入一次(注入时判断),在使用时根据key可以获取对象对应的单例。

这种方式可以让我们管理多种类型的单例,并且使用时通过统一的接口进行获取,例如在Android中,各种类型的服务在应用启动时都注册在Context内的一个容器中,我们需要使用各种服务时,可以通过Context的getService方法获得服务的单例,服务不能重复创建,会很消耗资源。

考虑反序列化

上面所介绍的6种实现单例的方法中,除了使用容器实现单例模式的方法,其他5种方法都有以下共同的关键点:

1、构造函数私有;

2、通过一个静态方法或枚举返回单例对象;

3、在多线程环境下,确保单例类的实例只有一个.

一般要实现单例模式,做到这3个点就行了,这样就能确保在同一个进程内单例类只有一个实例存在,但是,如果你的单例类是可以保存到磁盘或通过网络传输,换句话说你的单例类是支持序列化的,那么你就要保证单例类在反序列化时不会重新创建新的对象实例,因为反序列化时会创建一个和单例一模一样的实例的,java中通过ObjectInputStream的readObject方法来实现反序列化,它里面会通过反射来创建一个新的实例,所以就算你的构造方法私有,它还是可以通过setAccessible(true)来获得单例构造器的访问权,从而创建一个新的对象实例,反序列化的本质就是反射,换句话说,反射会破坏单例模式的实例唯一性

那么如何确保单例类反序化时不会重新创建新实例呢?只要在单例类中加入以下方法:

public class CEO{
    private static CEO mCeo = null;
    
    //...
    
    private Object readResolve() throws ObjectStreamException {
        //返回单例对象
        return mCeo;
    }
}

加入readResolve方法后就可以改变反序列化的规则,在readObject方法中,它发现该类定义了readResolve方法,它就会通过反射调用readResolve方法返回对象实例,而不是默认的通过反射重新创建一个新的对象,所以只要我们在readResolve方法中返回单例对象,readObject方法就会返回单例对象,这样就防止了单例模式被反序列化的破坏。

注意:对于枚举模式不存在反序列化重新生成实例的情况,所以对于枚举模式实现的单例不用考虑反序列化情况,因为枚举的反序列化不是通过反射实现的,而是通过其他方式实现,枚举有自己的一套处理反序列化的机制,类似于使用容器的方式,有兴趣可以自己查找资料,而对于饿汉模式、静态内部类形式、懒汉模式、DCL、容器中的单例就需要考虑反序列情况。

总结

本文讲解了6种方式实现单例模式,分别是饿汉方式、静态内部类形式、懒汉模式、DCL、枚举模式和容器方式,不管使用哪种形式实现单例,其核心思想都是以下4点:

1、构造函数私有化;

2、通过静态方法获取单例;

3、保证线程安全;

4、避免反序列化重新生成实例.

这6种方式的优缺点如下:

这么多种方式实现单例模式,我们如何选择呢?

单线程环境下工作,那么推荐你使用不加synchronized关键字的懒汉模式;

多线程环境下工作,这时就要考虑线程安全问题,基于类的初始化的单例模式天生线程安全,可以使用饿汉方式、静态内部类方式、枚举模式;如果你要明确的延迟初始化要求,推荐使用静态内部类方式、DCL、枚举模式;如果你有选择困难症,那不用考虑那么多了,推荐你使用DCL和静态内部类形式;不管在什么场合,都不要考虑使用加synchronized关键字的懒汉模式,它的缺点最多。

为什么android中不推荐使用枚举。

 enums会使得代码更易读更安全,但是在编写高效Android代码时避免使用枚举,我们在很多经典的Java书已经看到推荐使用枚举来代替int常量了,但是在Android开发中我不建议使用枚举,特别是大型的App中,能不用则不用。因为它会牺牲执行的速度和并大幅增加文件体积。这也是性能优化中减少OOM的一个方面。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值