创建型模式——单例模式

2020-08-10  本文已影响0人  Doooook

单例模式(singleton pattern),顾名思义,用来保证一个对象只能创建一个实例,除此之外,它还提供了对实例的全局访问方法。
单例模式的实现非常简单,只由单个类组成。为确保单例实例的唯一性,所有的单例构造器都要被声明为私有的(private),再通过声明静态(static)方法实现全局访问获得该单例实例。实现代码如下所示:

/**
 * @author: Jay Mitter
 * @date: 2020-08-09 21:40
 * @description: 懒汉式单利模式,实例为空的时候才去创建实例
 */
public class Singleton {

    private static Singleton instance;

    /**
     * 构造方法私有化
     */
    private Singleton() {
        // Instantiated: 被实例化
        System.out.println("Singleton is Instantiated.");
    }

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

    /**
     * 直接通过静态方法获取单一实例调用其方法:Singleton.getInstance().doSomething();
     */
    public void doSomething() {
        System.out.println("Something is Done.");
    }
}

同步锁单例模式

单例模式的实现代码简单且高效,但还需注意一种特殊情况,在多线程应用中使用这种模式,如果实例为空,可能存在两个线程同时调用getInstance方法的情况。如果发生这种情况,第一个线程会首先使用新构造器实例化单例对象,同时第二个线程也会检查单例实例是否为空,由于第一个线程还没完成单例对象的实例化操作,所以第二个线程会发现这个实例是空的,也会开始实例化单例对象。
上述场景看似发生概率很小,但在实例化单例对象需要较长时间的情况下,发生的可能性就足够高,这种情况往往不能忽视。
要解决这个问题很简单,我们只需要创建一个代码块来检查实例是否空线程安全。可以通过以下两种方式来实现:

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

完整代码:

/**
 * @author: Jay Mitter
 * @date: 2020-08-09 21:40
 * @description: 懒汉式单利模式,实例为空的时候才去创建实例
 */
public class Singleton {

    private static Singleton instance;

    /**
     * 构造方法私有化
     */
    private Singleton() {
        // Instantiated: 被实例化
        System.out.println("Singleton is Instantiated.");
    }

    /**
     * 这种方式在多线程的环境下存在线程安全问题,当创建实例的过程耗时较长,第二个线程会发现这个实例是空的,也会开始实例化单例对象。
     * @return
     */
    /*public static Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }*/

    /**
     * 增加synchronized关键字以保证其线程安全
     * @return
     */
    /*public static synchronized Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }*/

    /**
     * 用synchronized代码块包装if(instance==null)条件。在这一环境中使用synchronized代码块时,需要指定一个对象来提供锁,
     * Singleton.class对象就起这种作用
     * @return
     */
    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if (null == instance) {
                instance = new Singleton();
            }
        }
        return instance;
    }

    /**
     * 直接通过静态方法获取单一实例调用其方法:Singleton.getInstance().doSomething();
     */
    public void doSomething() {
        System.out.println("Something is Done.");
    }
}

拥有双重校验锁机制的同步锁单例模式

前面的实现方式能够保证线程安全,但同时带来了延迟。用来检查实例是否被创建的代码是线程同步的,也就是说此代码块在同一时刻只能被一个线程执行,但是同步锁(locking)只有在实例没被创建的情况下才起作用。如果单例实例已经被创建了,那么任何线程都能用非同步的方式获取当前的实例。只有在单例对象未实例化的情况下,才能在synchronized代码块前添加附加条件移动线程安全锁:

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

无锁的线程安全单例模式

Java中单例模式的最佳实现形式中,类只会加载一次,通过在声明时直接实例化静态成员的方式来保证一个类只有一个实例。这种实现方式避免了使用同步锁机制和判断实例是否被创建的额外检查(恶汉式):

/**
 * @author: Jay Mitter
 * @date: 2020-08-09 21:40
 * @description: 恶汉式单利模式,类初始化的时候就创建
 */
public class LockFreeSingleton {

    private static LockFreeSingleton instance = new LockFreeSingleton();

    /**
     * 构造方法私有化
     */
    private LockFreeSingleton() {
        // Instantiated: 被实例化
        System.out.println("Singleton is Instantiated.");
    }

    public static synchronized LockFreeSingleton getInstance() {
        return instance;
    }

    /**
     * 直接通过静态方法获取单一实例调用其方法:Singleton.getInstance().doSomething();
     */
    public void doSomething() {
        System.out.println("Something is Done.");
    }
}

提前加载和延迟加载

按照实例对象被创建的时机,可以将单例模式分为两类。如果在应用开始时创建单例实例,就称作提前加载单例模式;如果在getInstance方法首次被调用时才调用单例构造器,则称作延迟加载单例模式。
前面例子中描述的无锁线程安全单例模式在早期版本的Java中被认为是提前加载单例模式,但在最新版本的Java中,类只有在使用时候才会被加载,所以它也是一种延迟加载模式。另外,类加载的时机主要取决于JVM的实现机制,因而版本之间会有不同。所以进行设计时,要避免与JVM的实现机制进行绑定。
目前,Java语言并没有提供一种创建提前加载单例模式的可靠选项。如果确实需要提前实例化,可以在程序的开始通过调用getInstance方法强制执行,如下面代码所示:

Singleton.getInstance()

正确的双重检查锁

同步锁单例模式虽然解决了问题,但是因为用到了synchronized,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,之后的调用都没必要再进行加锁。这个时候就需要双重检测锁(double checked locking),是对上述问题的一种优化。先判断对象是否已经被初始化,再决定要不要加锁。

但是上面的双重检测锁也是有问题的,如果那样写,运行顺序就成了:

  1. 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回。
  2. 获取锁。
  3. 再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象。

执行双重检查是因为,如果多个线程同时通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。这样,除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题。

隐患

上述写法看似解决了问题,但是有个很大的隐患。实例化对象的那行代码(标记为error的那行),实际上可以分解成以下三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚分配的内存空间

但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

  1. 分配内存空间
  2. 将对象指向刚分配的内存空间
  3. 初始化对象

现在考虑重排序后,两个线程发生了以下调用:

image.png
在这种情况下,T7时刻线程B对uniqueSingleton的访问,访问的是一个初始化未完成的对象。

正确的双重检测锁:

public class Singleton {
    private volatile static Singleton uniqueSingleton;

    private Singleton() {
    }

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

为了解决上述问题,需要在uniqueSingleton前加入关键字volatile。使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。

至此,双重检查锁就可以完美工作了。

参考:https://www.cnblogs.com/xz816111/p/8470048.html

上一篇下一篇

猜你喜欢

热点阅读