Java设计模式之单例模式的究极版写法

redis_logo

单例模式可能是后端学习者接触到的第一种设计模式,可是单例模式真的有那么简单吗?在并发模式下会出现什么样的问题?在学习了前面的并发知识后,我们来看看究极版的单例模式应该怎么写。


一、单例模式第一版

我们最初接触到的单例模式一般就是懒汉模式与饿汉模式。我们先来看看怎么写:

//懒汉模式
public class Singleton {
    private Singleton() {}  //私有构造函数
    private static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
//饿汉模式
public class Singleton {
    private Singleton() {}  //私有构造函数
    private static Singleton instance = new Singleton();  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
        return instance;
    }
}
  • 要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。

  • instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。至于其中的区别后来会做解释。

  • getInstance是获取单例对象的方法。

这两个名字很形象:饿汉主动找食物吃,懒汉躺在地上等着人喂。
1、饿汉式:在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。
2、懒汉式:当程序第一次访问单件模式实例时才进行创建。

懒汉模式加载快执行慢,但是有线程安全问题,容易引起不同步问题,所以应该创建同步"锁"。

二、单例模式第二版

懒汉模式的线程安全问题主要在if (instance == null)这句判断是否为空上。在多线程的环境下,可能有多个线程同时通过这个判断。这样一来,就有可能同时创建多个实例。让我们来对代码做一下修改:

public class Singleton {
    private Singleton() {}  //私有构造函数
   private static Singleton instance = null;  //单例对象
   //静态工厂方法
   public static Singleton getInstance() {
        if (instance == null) {      //双重检测机制
            synchronized (Singleton.class){  //同步锁
                if (instance == null) {     //双重检测机制
                    instance = new Singleton();
                }
            }
         }
        return instance;
    }
}
  • 为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。

  • 进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。

然而,这种方法也有一定的缺席。

三、单例模式第三版

假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法。

这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到false。

我们之前在JAVA并发编程(一):理解volatile关键字学习过指令重排的知识,instance = new Singleton()这个操作不是一个原子操作,它在执行的时候要经历以下三个步骤:

memory =allocate();    //1:分配对象的内存空间 
ctorInstance(memory);  //2:初始化对象 
instance =memory;     //3:设置instance指向刚分配的内存地址 

所以这里有可能出现如下情况:

当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。

如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。

public class Singleton {
    private Singleton() {}  //私有构造函数
    private volatile static Singleton instance = null;  //单例对象
    //静态工厂方法
    public static Singleton getInstance() {
          if (instance == null) {      //双重检测机制
         synchronized (Singleton.class){  //同步锁
           if (instance == null) {     //双重检测机制
             instance = new Singleton();
                }
             }
          }
          return instance;
      }
}

三、其他方式实现单例模式

实现单例模式的手段还有很多,我们再来看一些别的实现方式。

①静态内部类实现单例模式

public class Singleton {
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

需要注意的是:

  • 从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。

  • INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

  • 静态内部类与饿汉&懒汉模式存在共同的问题:无法防止利用反射来重复构建对象。

②枚举实现单例模式

可以防止反射的无懈可击的单例模式代码:

public class SingletonExample {

    // 私有构造函数
    private SingletonExample() {

    }

    public static SingletonExample getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        INSTANCE;

        private SingletonExample singleton;

        // JVM保证这个方法绝对只调用一次
        Singleton() {
            singleton = new SingletonExample();
        }

        public SingletonExample getInstance() {
            return singleton;
        }
    }
}
  • 使用枚举实现的单例模式不仅能够防止反射构造对象,而且可以保证线程安全。不过这种方式也有一个缺点,那就是不能实现懒加载,它的单例模式是在枚举类被加载的时候进行初始化的。

参考文章

漫画:什么是单例模式?


本文作者: catalinaLi
本文链接: http://catalinali.top/2018/singletonPattern/
版权声明: 原创文章,有问题请评论中留言。非商业转载请注明作者及出处。

推荐阅读更多精彩内容