为何用枚举类来实现单例模式愈来愈流行?

前言

单例模式是 Java 设计模式中最简单的一种,只须要一个类就能实现单例模式,可是,你可不能小看单例模式,虽然从设计上来讲它比较简单,可是在实现当中你会遇到很是多的坑,因此,系好安全带,上车。java

单例模式的定义

单例模式就是在程序运行中只实例化一次,建立一个全局惟一对象,有点像 Java 的静态变量,可是单例模式要优于静态变量,静态变量在程序启动的时候JVM就会进行加载,若是不使用,会形成大量的资源浪费,单例模式可以实现懒加载,可以在使用实例的时候才去建立实例。开发工具类库中的不少工具类都应用了单例模式,比例线程池、缓存、日志对象等,它们都只须要建立一个对象,若是建立多份实例,可能会带来不可预知的问题,好比资源的浪费、结果处理不一致等问题。设计模式

单例的实现思路

  • 静态化实例对象
  • 私有化构造方法,禁止经过构造方法建立实例
  • 提供一个公共的静态方法,用来返回惟一实例

单例的好处

  • 只有一个对象,内存开支少、性能好缓存

  • 避免对资源的多重占用安全

  • 在系统设置全局访问点,优化和共享资源访问微信

单例模式的实现

单例模式的写法有饿汉模式、懒汉模式、双重检查锁模式、静态内部类单例模式、枚举类实现单例模式五种方式,其中懒汉模式、双重检查锁模式两种方式,若是你写法不当,在多线程状况下会存在不是单例或者单例出异常等问题,具体的缘由,在后面的对应处会进行说明。咱们从最基本的饿汉模式开始咱们的单例编写之路。多线程

饿汉模式

饿汉模式采用一种简单粗暴的形式,在定义静态属性时,直接实例化了对象。代码以下:app

//在类加载时就完成了初始化,因此类加载较慢,但获取对象的速度快
public class SingletonObject1 {
    // 利用静态变量来存储惟一实例
    private static final SingletonObject1 instance = new SingletonObject1();

    // 私有化构造函数
    private SingletonObject1(){
        // 里面可能有不少操做
    }

    // 提供公开获取实例接口
    public static SingletonObject1 getInstance(){
        return instance;
    }
}

复制代码

饿汉模式的优缺点

优势
  • 因为使用了static关键字,保证了在引用这个变量时,关于这个变量的因此写入操做都完成,因此保证了JVM层面的线程安全
缺点
  • 不能实现懒加载,形成空间浪费,若是一个类比较大,咱们在初始化的时就加载了这个类,可是咱们长时间没有使用这个类,这就致使了内存空间的浪费。

懒汉模式

懒汉模式是一种偷懒的模式,在程序初始化时不会建立实例,只有在使用实例的时候才会建立实例,因此懒汉模式解决了饿汉模式带来的空间浪费问题,同时也引入了其余的问题,咱们先来看看下面这个懒汉模式函数

public class SingletonObject2 {
    // 定义静态变量时,未初始化实例
    private static SingletonObject2 instance;

    // 私有化构造函数
    private SingletonObject2(){

    }

    public static SingletonObject2 getInstance(){
        // 使用时,先判断实例是否为空,若是实例为空,则实例化对象
        if (instance == null)
            instance = new SingletonObject2();
        return instance;
    }
}
复制代码

上面是懒汉模式的实现方式,可是上面这段代码在多线程的状况下是不安全的,由于它不能保证是单例模式,有可能会出现多份实例的状况,出现多份实例的状况是在建立实例对象时候形成的。因此我单独把实例化的代码提出,来分析一下为何会出现多份实例的状况。工具

1   if (instance == null)
     2       instance = new SingletonObject2();
复制代码

假设有两个线程都进入到 1 这个位置,由于没有任何资源保护措施,因此两个线程能够同时判断的instance都为空,都将去执行 2 的实例化代码,因此就会出现多份实例的状况。性能

经过上面的分析咱们已经知道出现多份实例的缘由,若是咱们在建立实例的时候进行资源保护,是否是能够解决多份实例的问题?确实如此,咱们给getInstance()方法加上synchronized关键字,使得getInstance()方法成为受保护的资源就可以解决多份实例的问题。加上synchronized关键字以后代码以下:

public class SingletonObject3 {
    private static SingletonObject3 instance;

    private SingletonObject3(){

    }

    public synchronized static SingletonObject3 getInstance(){
        /** * 添加class类锁,影响了性能,加锁以后将代码进行了串行化, * 咱们的代码块绝大部分是读操做,在读操做的状况下,代码线程是安全的 * */

        if (instance == null)
            instance = new SingletonObject3();
        return instance;
    }
}
复制代码

通过修改后,咱们解决了多份实例的问题,可是由于加入了synchronized关键字,对代码加了锁,就引入了新的问题,加锁以后会使得程序变成串行化,只有抢到锁的线程才能去执行这段代码块,这会使得系统的性能大大降低。

懒汉模式的优缺点

优势
  • 实现了懒加载,节约了内存空间
缺点
  • 在不加锁的状况下,线程不安全,可能出现多份实例
  • 在加锁的状况下,会是程序串行化,使系统有严重的性能问题

双重检查锁模式

再来讨论一下懒汉模式中加锁的问题,对于getInstance()方法来讲,绝大部分的操做都是读操做,读操做是线程安全的,因此咱们没必让每一个线程必须持有锁才能调用该方法,咱们须要调整加锁的问题。由此也产生了一种新的实现模式:双重检查锁模式,下面是双重检查锁模式的单例实现代码块:

public class SingletonObject4 {
    private static SingletonObject4 instance;

    private SingletonObject4(){

    }

    public static SingletonObject4 getInstance(){

        // 第一次判断,若是这里为空,不进入抢锁阶段,直接返回实例
        if (instance == null)
            synchronized (SingletonObject4.class){
                // 抢到锁以后再次判断是否为空
                if (instance == null){
                    instance = new SingletonObject4();
                }
            }

        return instance;
    }
}
复制代码

双重检查锁模式是一种很是好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去天衣无缝,实际上是存在问题,在多线程的状况下,可能会出现空指针问题,出现问题的缘由是JVM在实例化对象的时候会进行优化和指令重排序操做。什么是指令重排?,看下面这个例子,简单了解一下指令从排序

private SingletonObject4(){
     1   int x = 10;
     2   int y = 30;
     3  Object o = new Object();
                
    }
复制代码

上面的构造函数SingletonObject4(),咱们编写的顺序是一、二、3,JVM 会对它进行指令重排序,因此执行顺序多是三、一、2,也多是二、三、1,不论是那种执行顺序,JVM 最后都会保证因此实例都完成实例化。 若是构造函数中操做比较多时,为了提高效率,JVM 会在构造函数里面的属性未所有完成实例化时,就返回对象。双重检测锁出现空指针问题的缘由就是出如今这里,当某个线程获取锁进行实例化时,其余线程就直接获取实例使用,因为JVM指令重排序的缘由,其余线程获取的对象也许不是一个完整的对象,因此在使用实例的时候就会出现空指针异常问题。

要解决双重检查锁模式带来空指针异常的问题,只须要使用volatile关键字,volatile关键字严格遵循happens-before原则,即在读操做前,写操做必须所有完成。添加volatile关键字以后的单例模式代码:

// 添加volatile关键字
    private static volatile SingletonObject5 instance;

    private SingletonObject5(){

    }

    public static SingletonObject5 getInstance(){

        if (instance == null)
            synchronized (SingletonObject5.class){
                if (instance == null){
                    instance = new SingletonObject5();
                }
            }

        return instance;
    }
}
复制代码

添加volatile关键字以后的双重检查锁模式是一种比较好的单例实现模式,可以保证在多线程的状况下线程安全也不会有性能问题。

静态内部类单例模式

静态内部类单例模式也称单例持有者模式,实例由内部类建立,因为 JVM 在加载外部类的过程当中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由static修饰,保证只被实例化一次,而且严格保证明例化顺序。静态内部类单例模式代码以下:

public class SingletonObject6 {


    private SingletonObject6(){

    }
    // 单例持有者
    private static class InstanceHolder{
        private  final static SingletonObject6 instance = new SingletonObject6();

    }
    
    // 
    public static SingletonObject6 getInstance(){
        // 调用内部类属性
        return InstanceHolder.instance;
    }
}
复制代码

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较经常使用的一种单例模式。在没有加任何锁的状况下,保证了多线程下的安全,而且没有任何性能影响和空间的浪费。

枚举类实现单例模式

枚举类实现单例模式是 effective java 做者极力推荐的单例实现模式,由于枚举类型是线程安全的,而且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法很是简单,并且枚举类型是所用单例实现中惟一一种不会被破坏的单例实现模式。

public class SingletonObject7 {


    private SingletonObject7(){

    }

    /** * 枚举类型是线程安全的,而且只会装载一次 */
    private enum Singleton{
        INSTANCE;

        private final SingletonObject7 instance;

        Singleton(){
            instance = new SingletonObject7();
        }

        private SingletonObject7 getInstance(){
            return instance;
        }
    }

    public static SingletonObject7 getInstance(){

        return Singleton.INSTANCE.getInstance();
    }
}
复制代码

破坏单例模式的方法及解决办法

一、除枚举方式外, 其余方法都会经过反射的方式破坏单例,反射是经过调用构造方法生成新的对象,因此若是咱们想要阻止单例破坏,能够在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法以下:

private SingletonObject1(){
    if (instance !=null){
        throw new RuntimeException("实例已经存在,请经过 getInstance()方法获取");
    }
}
复制代码

二、若是单例类实现了序列化接口Serializable, 就能够经过反序列化破坏单例,因此咱们能够不实现序列化接口,若是非得实现序列化接口,能够重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。

public Object readResolve() throws ObjectStreamException {
        return instance;
    }
复制代码

若是你以为文章不错,欢迎点赞转发

最后

打个小广告,欢迎扫码关注微信公众号:「平头哥的技术博文」,一块儿进步吧。

平头哥的技术博文
相关文章
相关标签/搜索