设计模式之单例模式--懒汉模式与饿汉模式(2)

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

上一章讲了单例模式的基本实现方式,但是在多线程环境下,这种方式是存在严重的问题的。所以本章我们就来解决多线程环境的问题。

懒汉模式

利用方法锁实现

一种方式是在获取实例的方法上加锁

// 代码清单1

public class LazyInstantiationConfigUtils {

    private static Logger logger = LoggerFactory.getLogger(SimpleConfigUtils.class);
    private static LazyInstantiationConfigUtils instance = null;

    private Map<String, String> props = new HashMap<>();

    /**
     * 单例的目的就是为了控制实例数量,所以构造函数一定要私有化,这样才能被其他使用者不能够随便实例化对象。
     */
    private LazyInstantiationConfigUtils() {
        this.readFromConfigFile();
        logger.info("对象实例化完成。");
    }

    /**
     * 获取配置实例,方法上加锁。当多个线程同时调用时只有一个线程可以访问方法,其他线程将被阻塞。
     * @return 配置实例
     */
    public static synchronized LazyInstantiationConfigUtils getInstance() {
        if (instance == null) {
            instance = new LazyInstantiationConfigUtils();
        }
        return instance;
    }

    /**
     * 根据配置项的名称获取配置项的值
     * @param propName 配置项的名称
     * @return 配置项的值
     */
    public String getPropertyValue(String propName) {
        return this.props.get(propName);
    }

    private void readFromConfigFile() {
        logger.info("假装从配置文件中读取了配置");
        props.put("application.name", "DesignPattern");
        props.put("application.type", "SpringBoot");
    }
}

这种方式简单,但是性能差。每次调用都会判断是否有锁,在同一时刻只能有一个线程调用。

双重检查锁定(Double-Check Locking)

另外一种方式是,使用双重检查锁定(Double-Check Locking)或者叫双重锁定机制(double locking mechanism)。

// 代码清单2

/**
 * 获取配置实例
 * @return 配置实例
 */
public static LazyInstantiationConfigUtils getInstance() {
    if (instance == null) {
        synchronized (LazyInstantiationConfigUtils.class) { // (1)
            if (instance == null) { // (2)
                instance = new LazyInstantiationConfigUtils(); // (3)
            }
            
        }
    }
    return instance;
}

指令重排问题(Out-of-Order)

问题就在于(3),在构造函数执行之前,变量instance变为了non-null
想象有两个线程,执行顺序如下

  1. Thread 1 进入getInstance()方法
  2. Thread 1 进入同步块,也就是(1),因为这是instancenull
  3. Thread 1 执行了代码(3),但是构造函数还没有执行。这时instance变为了non-null
  4. Thread 2 抢占了 Thread 1的资源,开始执行
  5. Thread 2 进入getInstance()方法
  6. Thread 2 判断instance不为null,并把为初始化完成的instance返回

问题已经出现了, Thread 2获取到了一个未初始化的对象。那么在使用的时候肯定也是有问题的。如何解决这个问题呢?

第一种方式是,使用两段双重锁定机制,如下。

/**
 * 获取配置实例
 * @return 配置实例
 */
public static LazyInstantiationConfigUtils getInstance() {
    if (instance == null) {
        synchronized (LazyInstantiationConfigUtils.class) {
            LazyInstantiationConfigUtils inst = instance;
            if (inst == null) {
                synchronized (LazyInstantiationConfigUtils.class) {
                    inst = new LazyInstantiationConfigUtils();
                }
                instance = inst;
            }
        }
    }
    return instance;
}

这种方式可以保证在构造函数未执行完之前,instance一直是null

另一种方法是给instance变量增加volatile修饰符。使用volatile可以保证执行顺序的正确性,保证变量的读在写之后。但是也会带来新的问题。

  • volatile会阻止指令重拍,这样就会屏蔽掉JVM所做的优化,降低了程序的执行效率。
  • 许多虚拟机并没有实现volatile的顺序一致性(Sequential Consistency)。比如在1.5以下的版本中。

饿汉模式

饿汉模式就是在定义静态变量时就实例化对象,因此在类加载的时候就完成了对象的实例化,而并不是在类第一次使用时。

public class EarlyInstantiationConfigUtils {

    private static EarlyInstantiationConfigUtils instance = new EarlyInstantiationConfigUtils();

    private EarlyInstantiationConfigUtils() {
        // 初始化过程
    }

    /**
     * 获取配置实例
     * @return 配置实例
     */
    public static EarlyInstantiationConfigUtils getInstance() {
        return instance;
    }
}

这种方式是最简单的方式,但是会造成一定的资源浪费。因为无论你使用与否,JVM都会在类加载的时候完成类的实例化。

Initialization on Demand Holder (IoDH)

这种方式可以实现实例的懒加载。

public class EarlyInstantiationConfigUtils {

    private EarlyInstantiationConfigUtils() {
        // 初始化过程
    }
    
    private static class DemandHolder {
        private static EarlyInstantiationConfigUtils instance = new EarlyInstantiationConfigUtils();
    }

    /**
     * 获取配置实例
     * @return 配置实例
     */
    public static EarlyInstantiationConfigUtils getInstance() {
        return DemandHolder.instance;
    }
}

在类加载时,由于instance不是类的静态成员变量,所以不会初始化。在调用getInstance()方法时,加载DemandHolder类,这时instance作为静态成员变量,开始初始化。这是由Java虚拟机来保证线程安全的,并确保只有一个实例被创建。

通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能。

参考资料

JSR-133 Java Memory Model and Thread Specification 1.0 Proposed Final Draft
Double-checked locking and the Singleton pattern
Singleton Pattern
Singleton and its variance (Contextual and Conditional) and scope in WAS 7

猜你喜欢

转载自blog.csdn.net/jaune161/article/details/87880544