设计模式1_单例模式原理的思考

1.前言

关于设计模式的文章一直以来都没有写,因为总感觉翻一遍书,只是用代码搞个什么Cat,Dog的Class,再来个eat的method,abstract个animal来敲遍代码太无趣了,没有实际应用,或者深入思考,照书贴一遍代码是浪费时间的。今天之所以写下这一篇自然是有一些有趣并且有内涵的东西可以让人思考,那开始吧。

2.单例的思考

一般来说单例的初始化根据加载的时机分为两种,饿汉式和懒汉式,其中懒汉式又分为静态内部类和双重锁两种实现。

而这次,我们按并发场景下实现线程安全原理来分,基于类加载机制和基于双重锁机制。

2.1基于类加载机制

首先,饿汉式,

public class Singleton {
    private static final Singleton INSTANCE=new Singleton();

    public static getInstance(){
        return INSTANCE;
    }
}

然后是懒汉式中的静态内部类。因为类在第一次使用时才会被加载,所以能实现延迟加载。

public class Singleton {

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

这两者之所以线程安全,都是源于在java的类加载机制中,类只会被初始化一次,静态常量字段也只会初始化一次。

那JVM如何保证类只会被加载一次?看一下CLassLoader的loadClass方法片段:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);

注意到显示的使用了synchronized关键字来获取加载锁,因此这就是类加载是线程安全的原因。

2.2双重锁机制

代码:

public class DoubleCheckSingleton {
    private static volatile DoubleCheckSingleton instance;

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

由于synchronized存在性能开销,因此人们发明了这种双重锁机制来完美的实现延迟加载。如果第一次检查INSTANCE不为null,那么就不需要执行下面的加锁和初始化操作。
注意到双重锁实现类的静态常量字段使用了一个关键字:volatile,在并发情景下这是必须的.

在执行语句INSTANCE = new Instance();时,JVM层次可以认为做了这么三个操作

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

但JVM为了优化性能,会有一个优化操作叫指令重排序。上面的指令可能会被优化成这样

memory = allocate();   //1:分配对象的内存空间
instance = memory;     //3:设置instance指向刚分配的内存地址
                       //注意,此时对象还没有被初始化!
ctorInstance(memory);  //2:初始化对象

也就是说当如果发生了重排序,线程读取到INSTANCE不为null时,INSTANCE对象可能没有初始化完毕。使用未初始化的对象很有可能导致异常。

而谈到volatile关键字作用时,其中一个就是防止并发情景下JVM进行指令重排序。

至于为何能防止重排序,其关键在于volatile变量在操作时会增加一个指令lock addl $0x0, (%rsp),,让变量的修改实时同步到内存,形成其他指令无法被重排序效果。

如果再抠一下细节,注意到双重锁模式下使用到了一个临时变量。

    public static DoubleCheckSingleton getInstance() {
        DoubleCheckSingleton result = instance;
        ...
        return result;
    }

其原因在于上面提到的lock指令是相对耗性能的,这个临时变量能减少一次lock,压榨出极限性能。

以上呢这便是双重锁机制实现的完整解析。

结语

虽然这只是是设计模式中最简单的单例模式,但深入后发现竟可以延伸出对类加载,synchronized,volatile等java原理的思考,一开始是没想到的,的确是一些挺有意思的东西。更多关于这方面原理的细节推荐以下文章。

双重检查锁定与延迟初始化
深入理解java内存模型系列文章

Comments
Write a Comment