欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

看似简单并不简单的单例模式

程序员文章站 2022-03-24 18:44:46
...

引言

今天无意间在公众号上看到一篇文章,文章中问到了一个问题,如何在不使用 synchronized 和 Lock 锁的情况下,实现单例模式?说实话,在此之前,只知道单例模式的两种实现模式,其他的压根见都没见过。今天算是开了眼界了,还是才学疏浅,路还很长啊!那就借此机会,趁热总结。

常见的单例模式

基于volatile的解决方案

这种模式其实就是我们俗称的懒汉模式,也叫双重检查锁定。在多线程环境中,为了保证类初始化只被初始化一次,就需要使用锁互斥保证原子性。为什么要加两层锁呢?这是为了防止发生竟态条件问题,再就是由于编译优化会带来有序性问题。这种模式也很容易理解,以下给出代码:

/*
    基于volatile的解决方案
 */
public class DoubleCheckedLocking {
    //volatile保证实例原子性
    private static volatile Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLocking.class) {
                if (instance == null) {
                    instance = new Instance();
                }
            }
        }
        return instance;
    }
}

class Instance { }

延迟初始化

接下来就是与之对应的饿汉模式,也叫延迟初始化。这种模式也很好理解,借助class的类加载机制实现线程安全单例。

public class Singleton { 
    private static Singleton instance = new Singleton();
    private Singleton (){}
    public static Singleton getInstance() {
      return instance;
    }
}

这种方案还有一种变种形式:

public class Singleton {
    private Singleton instance = null;
    static {
        instance = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return this.instance;
    }
}

此外,还有更高级一点的写法,使用静态内部类:

/*
    基于类初始化的解决方案
 */
public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance;
    }
}

这种方式相比前面两种有所优化,就是使用了 lazy-loading 。 Instance 类被加载了,但是 instance 并没有立即被初始化。因为 InstanceHolder 类并没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 InstanceHolder 类,从而实例化 instance 。

CAS单例模式

以上就是最基本的单例模式实现方式,接下来就是利用我们熟知的CAS机制实现乐观锁的单例模式:

/*
    CAS实现单例模式
 */
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();

    private Singleton() {}

    public static Singleton getInstance() {
        for (; ; ) {
            Singleton singleton = INSTANCE.get();
            if (singleton != null) {
                return singleton;
            }

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

另外,如果N个线程同时执行到 singleton = new Singleton(); 的时候,会有大量对象创建,很可能导致内存溢出所以,不建议使用这种实现方式。

使用枚举类实现单例模式

重头戏来了,这可以是大名鼎鼎的《Effective Java》中推荐的单例模式实现方法!因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被作者认为是实现 Singleton 的最佳方法。

看源码:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {}

可以看出它实现了 Comparable 和 Serializable 接口,其实 Enum 就是一个普通的类,它继承自 java.lang.Enum 类。在JDK5 中提供了大量的语法糖,枚举就是其中一种。枚举也是语法糖的一种。
所谓语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是但是更方便程序员使用。只是在编译器上做了手脚,却没有提供对应的指令集来处理它。

用枚举类实现单例模式非常简单:

public enum Singleton{
    INSTANCE;
    
    public void whateverMethod(){}
}

我们来用模拟下枚举类实现数据库连接场景:

public enum SingletonOfEnum {
    DATASOURCE;
    private MYSQLConnection connection = null;

    private SingletonOfEnum() {
        connection = new MYSQLConnection();
    }

    public MYSQLConnection getConnection() {
        return connection;
    }

}

class MYSQLConnection{}

主方法:

public class Main {
    public static void main(String[] args) {
        MYSQLConnection c1 = SingletonOfEnum.DATASOURCE.getConnection();
        MYSQLConnection c2 = SingletonOfEnum.DATASOURCE.getConnection();
        MYSQLConnection c3 = SingletonOfEnum.DATASOURCE.getConnection();

        System.out.println(c1 == c2);
        System.out.println(c1 == c3);
        System.out.println(c2 == c3);
    }
}

输出结果:

看似简单并不简单的单例模式

由此可见,三次返回的都是同一个实例!

那么枚举类是怎么在多线程情况下实现线程安全的?

反编译代码:

public final class DesignPattern.SingletonPattern.EnumSingleton.SingletonOfEnum extends java.lang.Enum<DesignPattern.SingletonPattern.EnumSingleton.SingletonOfEnum> {
  public static final DesignPattern.SingletonPattern.EnumSingleton.SingletonOfEnum DATASOURCE;

由反编译后的代码可知,DATASOURCE 被声明为 static 的,根据饿汉模式中所描述的类加载过程,可以知道虚拟机会保证一个类的 <clinit>() 方法 在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。

那么序列化是怎么回事呢?

如果看Enum源码,会发现有一个valueOf() 方法:

public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
}

Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
也就是说,以下面枚举为例,序列化的时候只将 DATASOURCE 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

由此可得出结论:枚举类本身就保证序列化单例。