延迟初始化的线程安全实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kidthephantomthiefI/article/details/82999666

1. 延迟初始化的不恰当实现

  • 非线程安全版本

单例模式中,很多时候会使用延迟初始化(lazy initialization),即在第一次使用时才初始化对象,以推迟高开销的对象初始化工作。下面是非线程安全的延迟初始化代码:

public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (null == uniqueSingleton) {
            uniqueSingleton = new Singleton();
        }
        return uniqueSingleton;
    }
}

这段代码的问题在于,在多线程的情况下,可能会出现如下访问顺序。

Time Thread A Thread B
T1 检查到uniqueSingleton为空
T2 检查到uniqueSingleton为空
T3 初始化对象A
T4 返回对象A
T5 初始化对象B
T6 返回对象B

这两个线程就持有了这个类的两个不同实例,违背了单例模式的初衷。

  • 同步锁版本

我们可以用synchronized关键字修饰方法,加上同步锁。

public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (null == uniqueSingleton) {
            uniqueSingleton = new Singleton();
        }
        return uniqueSingleton;
    }
}

但是synchronized会导致性能开销,而且其实只用在第一次初始化时加锁,后面再调用时,都没有必要加锁了。

  • 错误的双重检查锁版本

那我们可以考虑做一下优化,使用双重检查锁——在加锁前先做一次检查,如果对象初始化已经完成,就没有必要再加锁了,代码如下。

public class Singleton {
    private static Singleton uniqueSingleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton(); // error
                }
            }
        }
        return uniqueSingleton;
    }
}

这样做,似乎完美解决了产生多个对象和性能开销的问题:

  1. 当多个线程通过了第一次检查,只有一个线程能首先获得同步锁、通过第二次检查并创建对象,后续已通过第一次检查,再获取同步锁的线程会由于第二次检查不通过而不必创建对象,而还未通过第一次检查的线程会由于第一次检查未通过而不必创建对象。
  2. 不必将所有线程同步,当有线程创建了对象后,其他所有线程都可以任意检查,发现对象已被初始化并且返回。

实则不然。

2. 线程安全的延迟初始化——基于volatile的双重检查锁实现

上面双重检查锁的代码其实是错误的,问题的根源在于初始化对象并不是原子操作,并且可能出现重排序。初始化对象的代码,即上面代码中标记了error的那一行,可以分解为三个步骤:
i. 分配内存空间
ii. 初始化对象
iii. 将对象指向刚分配的内存空间
而在一些JIT编译器上,为了性能原因,第二步和第三步可能被重排序。两个线程可能出现如下的执行顺序:

Time Thread A Thread B
T1 检查到uniqueSingleton为空
T2 获取锁
T3 再次检查到uniqueSingleton为空
T4 为uniqueSingleton分配内存空间
T5 将uniqueSingleton指向内存空间
T6 检查到uniqueSingleton不为空
T7 访问uniqueSingleton(未初始化的对象)
T8 初始化uniqueSingleton

这时,线程B会读到一个未被初始化的对象(一块具有随机值的内存)。为了解决这个问题,只需要用volatile关键字修饰变量uniqueSingleton就可以了,正确的双重检查锁的实现代码如下。

public class Singleton {
    private volatile static Singleton uniqueSingleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

使用volatile后,重排序被禁止,所有的写操作(write)都会发生在读操作(read)之前。

3. 线程安全的延迟初始化——基于类初始化锁的实现

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案(这个方案被称之为initialization-on-demand holder idiom),代码如下。

public class Singleton {
    private static class SingletonHolder {
        public static Singleton uniqueSingleton = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.uniqueSingleton;
    }
}

这个方案利用了Java中类的初始化锁(initialization lock)LC。根据JLS(Java SE 8 Edition)中“§12.4.1 When Initialization Occurs”一节中的描述:

A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

  • T is a class and an instance of T is created.
  • A static method declared by T is invoked.
  • A static field declared by T is assigned.
  • A static field declared by T is used and the field is not a constant variable (§4.12.4).
  • T is a top level class (§7.6) and an assert statement (§14.10) lexically nested within T (§8.1.3) is executed.

上述代码中当getInstance()方法第一次被调用时,符合上面的第四种情况,类SingletonHolder被初始化,会使用LC进行同步。这样就利用了由JVM实现的类初始化锁,实现了延迟初始化的线程安全版本。关于LC的具体细节可以查看参考文献[5]中的“§12.4.2. Detailed Initialization Procedure”一节。

4. 参考文献:

[1] 双重检查锁定与延迟初始化
http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization#anch136785
[2] Java中的双重检查锁(double checked locking)
https://www.cnblogs.com/xz816111/p/8470048.html
[3] Double-checked locking and the Singleton pattern
https://www.ibm.com/developerworks/java/library/j-dcl/index.html
[4] Double-checked locking
https://en.wikipedia.org/wiki/Double-checked_locking
[5] The Java Language Specification, Java SE 8 Edition
https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4

猜你喜欢

转载自blog.csdn.net/kidthephantomthiefI/article/details/82999666