Back
Featured image of post 设计模式-单例模式

设计模式-单例模式

一、模式简介

在有些时候某个对象我们只需要一个,比如说线程池、缓存、对话框、注册表、日志等对象,在平时的工作中这种只能创建一个实例的对象,如果出现多个可能就会导致错误,这时就应该使用单例模式来构造出这种"唯一”的对象。单例模式:确保一个类只有一个实例,并提供一个全局访问点。

二、模式详解

单例设计模式的初衷就是确保一个类最终只有一个实例,单例模式也提供了这个实例的全局访问点。因为单例模式同时解决了两个问题,它是不满足单一职责原则的,但是并不妨碍其强大的功能。在java中实现单例设计模式需要一个私有的构造器、一个静态方法和一个静态变量。

下面我们来看一下最原始的单例模式实现:

  • 原始单例模式
/**
 * 单例
 *
 */
public class Singleton {
    /**
     * 静态变量
     */
    private static Singleton uniqueInstance;

    /**
     * 私有构造器
     */
    private Singleton(){}

    /**
     * 静态方法(全局访问点)
     */
    public static Singleton getInstance(){
        if (uniqueInstance==null){
            uniqueInstance=new Singleton();
        }
        return uniqueInstance;
    }
}

通过上面最原始的单例模式单例模式代码,来分析一下单例的生产过程:

  • 利用一个静态变量来记录Singleton的唯一值。
  • 把构造器声明为私有,只有Singleton类内部可以调用。
  • 用getInstance()方法实例化对象,并返回这个实例(请注意如果我们不要需要这个实例就永远不会产生。这就是"“延迟化实例”)

但是上面的单例模式有一个很重要的问题,那就是在并发情况下会破坏单例。比如当并发访问的时候,第一个调用getInstance方法的线程A,在判断完singleton是null的时候,线程A就进入了if块准备创造实例,但是同时另外一个线程B在线程A还未创造出实例之前,就又进行了singleton是否为null的判断,这时singleton依然为null,所以线程B也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就造成单例模式并非单例。那么解决它最容易想到的办法就是将整个代码给同步,就有如下这种形式:

  • 方法加锁
/**
 * 单例
 *
 */
public class Singleton {
    /**
     * 静态变量
     */
    private static Singleton uniqueInstance;

    /**
     * 私有构造器
     */
    private Singleton(){}

    /**
     * 静态方法(全局访问点)
     */
    public synchronized static Singleton getInstance(){
        if (uniqueInstance==null){
            uniqueInstance=new Singleton();
        }
        return uniqueInstance;
    }
}

通过加锁可以解决并发出现的问题,但是仔细想想,是不是只有当我们第一次调用这个方法的时候,才需要真正的同步?之后每次调用这个方法同步都是一种累赘。那么是否可以进行优化解决这个问题?下面提供两种解决方案:第一种通过"急切"创建实例,而不用延迟实例化的做法(也叫饿汉式)解决线程问题。第二种通过双重加锁的方式,在第一次调用时解决线程问题。

  • “急切"创建实例(饿汉式)
/**
 * 单例
 *
 */
public class Singleton {
    /**
     * 静态变量
     */
    private static Singleton uniqueInstance=new Singleton();

    /**
     * 私有构造器
     */
    private Singleton(){}

    /**
     * 静态方法(全局访问点)
     */
    public synchronized static Singleton getInstance(){
        return uniqueInstance;
    }
}

利用这个做法,我们依赖JVM在加载类时马上创建此唯一的单件实例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例。 从而很好的避免线程安全带来的问题。(但其并非"延迟加载实例”,可以通过包装内部类解决这样的问题)

  • 双重加锁
/**
 * 单例
 *
 */
public class Singleton {
    /**
     * 静态变量
     */
    private volatile static Singleton uniqueInstance;

    /**
     * 私有构造器
     */
    private Singleton(){}

    /**
     * 静态方法(全局访问点)
     */
    public synchronized static Singleton getInstance(){
        if (uniqueInstance==null){
            synchronized (Singleton.class){
                if (uniqueInstance==null){
                    uniqueInstance=new Singleton();
                }
            }

        }
        return uniqueInstance;
    }
}

我们先不关注修饰静态变量的Volatile关键字,先来关注getInstance()本身。这种做法与上面那种方法同步做法相比就要好很多了,因为我们只是在当前实例为null,也就是实例还未创建时才进行同步,否则就直接返回,这样就节省了很多无谓的线程等待时间,值得注意的是在同步块中,我们再次判断了synchronizedSingleton是否为null。因为可能两个线程同时进入第一个if(uniqueInstance==null),当A线程创建好对象释放掉锁,B线程获取锁又创建了一个对象这种情况。因此需要在同步代码块中再次加入一个判断。

经过这些可能我们会觉得代码已经天衣无缝了,那么为什么还要通过volatile修饰静态变量呢?

首先要明白在JVM创建新的对象时,主要要经过三步。

1.分配内存

2.初始化构造器

3.将对象指向分配的内存的地址

​ 这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了。

​ 因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给Singleton,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为Singleton对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了Singleton,就会产生莫名的错误。因此我们可以通过Volatile关键字确保创建对象时的有序性来解决这一问题。

三、适用场景

  • 如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
  • 如果你需要更加严格地控制全局变量, 可以使用单例模式。