高并发下如何保证单例模式的线程安全

单例模式是常用的软件设计模式之一,同时也是设计模式中最简单的形式之一,在单例模式中对象只有一个实例存在。单例模式的实现方式有两种,分别是懒汉式和饿汉式。

1、饿汉式

饿汉式在类加载时已经创建好实例对象,在程序调用时直接返回该单例对象即可,即在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建,如下是饿汉式的代码:

public class Singleton { 
  private static Singleton singleton = new Singleton(); 
  /** 
   * 私有化构造方法 
   */ 
  private Singleton(){ 

  } 

  /** 
   * 直接调用方法获取单例对象 
   */ 
  public static Singleton getSingleton() { 
    return singleton; 
  } 

}

如果创建的单例对象比较大,由于在类加载的过程中就加载了,那么会影响应用程序启动的速度;饿汉式不会存在线程安全的问题,因为类加载的过程中就创建了,并且程序只会运行一次。

2、懒汉式

懒汉式指全局的单例实例在第一次被使用时再创建,后面就不会创建实例对象,代码如下所示:

public class Singleton { 
  private static Singleton singleton; 

  /** 
   * 私有化构造方法 
   */ 
  private Singleton(){ 

  } 
  /** 
   * 判断当前的对象是否存在,如果存在就直接返回,如果不存在就创建一个对象 
   */ 
  public static Singleton getSingleton() { 
    if (singleton==null) { 
        singleton = new Singleton(); 
      } 
    return singleton; 
  } 

}

高并发下,上述的懒汉式会存在线程安全问题(会创建多个实例对象),如下所示:

如果线程1和线程2同时调用getSingleton方法时候,并且都通过了第17行代码的检查,那么线程1和线程2就创建了两个对象出来了。那么懒汉式如何保证线程安全呢?

(2.1)方法级别锁

如果直接在方法级别上增加锁,可以解决线程安全的问题,但是在高并发下会导致性能下降(因为多个线程执行到这个方法之后就会阻塞等待锁资源)。

众所周知,锁是用来锁住临界资源(即就是多线程同时竞争的资源),在懒汉模式中临界资源是创建对象这段代码(singleton = new Singleton();),那么锁只需要锁住创建对象的代码就可以了。

(2.2)同步代码块的锁

这里为什么要加一个先判断空的操作呢?目的是为了提升性能,因为一旦对象创建好了之后,后面的线程直接判断对象是否创建好了,创建好了之后在高并发下线程就不需要在锁位置阻塞等待了。但是这种方式在高并发下也是存在线程安全的问题,如下所示:

假设线程1和线程2同时到了17行代码处,并且当前的对象也是null,此时线程1先获取到锁,线程1下创建了一个新对象完成后锁释,随后线程2获取到锁后也创建了一个对象,那么这就无法保证只有一个单例对象。所以锁内部还需要再增加一个检查,如下所示:

当上一个获取锁的线程创建对象成功之后,下一个线程获取到锁的时候,再去判断一下这个对象是否创建成功,如果创建成功就不再创建新的对象。这种方法我们称为Double Check Lock,完整的代码如下所示:

public class Singleton { 
  private static Singleton singleton; 

  /** 
   * 私有化构造方法 
   */ 
  private Singleton(){ 

  } 
  /** 
   * 判断当前的对象是否存在,如果存在就直接返回,如果不存在就创建一个对象 
   */ 
  public static Singleton getSingleton() { 
    if (singleton==null) { 
        synchronized(Singleton.class) { 
            if (singleton==null) { 
               singleton = new Singleton(); 
             } 
          } 
      } 
    return singleton; 
  } 

}

但是在高并发这块还是存在线程的安全问题,因为创建对象的过程有三个步骤,如下所示:

(1)内存分配和赋予默认值

(2)执行初始化方法赋予初始化值

(3)建立指针指向堆上对象

如果在高并发下,假设步骤2和步骤3发生了指令重排,此时就可能会出现如下的情况:

栈里面的指针指向堆上的对象Singleton,但是现在由于指令重排指向一个空(空表示内存上真的什么都没有,连对象都解析不出来),这就出现一系列的问题,所以这里我们就需要禁止指令重排,所以我们需要添加volatile关键字来限制执行重排。

完整的代码的如下所示:

public class Singleton { 
  private static volatile Singleton singleton; 

  /** 
   * 私有化构造方法 
   */ 
  private Singleton(){ 

  } 
  /** 
   * 判断当前的对象是否存在,如果存在就直接返回,如果不存在就创建一个对象 
   */ 
  public static Singleton getSingleton() { 
    if (singleton==null) { 
        synchronized(Singleton.class) { 
            if (singleton==null) { 
               singleton = new Singleton(); 
             } 
          } 
      } 
    return singleton; 
  } 

}

总结:

(1)饿汉式在类加载的时候就实例化,并且创建单例对象,饿汉式无线程安全问题。

(1)懒汉式默认不会实例化,外部什么时候调用什么时候创建。懒汉式在多线程下是线程不安全的,所以我们可以通过双重检查的方式来保证其线程安全问题。

3