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

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

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

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 (singletnotallow==null) {
        synchronized(Singleton.class) {
            if (singletnotallow==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 (singletnotallow==null) {
        synchronized(Singleton.class) {
            if (singletnotallow==null) {
               singleton = new Singleton();
             }
          }
      }
    return singleton;
  }
  
}

总结:

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

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

责任编辑:武晓燕 来源: 龙虾编程
相关推荐

2020-07-15 08:14:12

高并发

2021-03-28 09:45:05

幂等性接口数据

2022-09-16 08:42:23

JavaAPI变量

2022-06-12 06:45:26

高并发防重

2014-08-08 13:30:44

Nginx

2013-01-30 10:12:24

NginxNginx优化高并发

2023-08-25 08:06:20

CPUMySQL线程

2024-06-17 00:02:00

线程安全HashMapJDK 1.7

2023-01-26 02:07:51

HashSet线程安全

2021-02-07 23:58:10

单例模式对象

2024-05-20 13:13:01

线程安全Java

2019-10-30 16:54:08

golangredis数据库

2024-03-13 15:18:00

接口幂等性高并发

2021-03-02 08:50:31

设计单例模式

2021-09-07 10:44:35

异步单例模式

2021-02-01 10:01:58

设计模式 Java单例模式

2024-02-02 11:24:00

I/O高并发场景

2023-02-03 15:16:42

SpringHystrix

2022-02-06 22:30:36

前端设计模式

2021-03-15 07:02:02

java线程安全
点赞
收藏

51CTO技术栈公众号