双重检查锁定(Double-Checked Locking)的问题和解决方案

这里写目录标题

  • 1.为什么要使用双重检查索引
    • 1.1 单例模式(懒汉式)存在的问题
    • 1.2 使用synchronized保证线程安全的延迟初始化对象
    • 1.3 为啥要引入双重检查锁定
  • 2.解决方式
    • 2.1 基于volatile的解决方案

1.为什么要使用双重检查索引

1.1 单例模式(懒汉式)存在的问题

public class Singleton {
    
    private static Singleton single = null;

    //静态工厂方法
    public static Singleton getInstance() {
        if (single == null) {             // 1.线程A执行
            single = new Singleton();     // 2.线程B执行
        }
        return single;
    }
}

存在的问题

  • 第一种情况
    1、线程A执行到了代码1的位置
    2、线程B执行到了代码2的位置, 但是还未执行, 正准备创建对象
    3、线程A看到single还未被实例化, 就会进入这个判断体重, 再次实例化对象
    4、两个线程都实例化对象, 无法保证单例模式
  • 第二种情况
    1、线程A执行到了代码1的位置
    2、线程B执行new Singleton()操作时发生了指令重排, 重排后的指令:先分配内存, 然后赋值给single, 然后再进行初始化(赋值和初始化两个指令被重排了)
    3、线程B赋值给single, 此时线程A正好判断instance !=null, 就返回了single
    4、此时就会读取到尚未初始化完成的single对象

1.2 使用synchronized保证线程安全的延迟初始化对象

public class Singleton {
    
    private static Singleton single = null;

    //静态工厂方法
    public synchronized  static Singleton getInstance() {
        if (single == null) {             // 1.线程A执行
            single = new Singleton();     // 2.线程B执行
        }
        return single;
    }
}

存在的问题:
1.由于对getInstance()方法做了同步处理,synchronized将导致性能开销。
2.如果getInstance()方法被多个线程频繁的调用,将会导致程序执行性能的下降。
3.如果getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供令人满意的性能。


1.3 为啥要引入双重检查锁定

由于synchronized存在巨大的性能开销。因此,人们想出了一个“聪明”的技巧:双重检查锁定【Double-Checked Locking】。人们想通过双重检查锁定来降低同步的开销。

线程不安全的双重检查锁定

public class Singleton {
    
    private static Singleton single = null;

    //静态工厂方法
    public static Singleton getInstance() {
        if (single == null) {							// 1处、第一次检查
        	// 只有当单例对象为空时, 才实例化对象(同步锁)
        	synchronized(Singleton.class) {				// 2处、加锁
        		if (single == null) {					// 3处、第二次检查
        			single = new Singleton();			// 4处、实例化对象【这里会出问题的】
        		}
        	}
        }
        return single;
    }
}

在1处,如果是第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。
在2处,如果多个线程试图在同一时间创建对象时,这里有同步代码块,会通过加锁来保证只有一个线程能创建对象。
在3处,获得锁的线程,会二次检查这个对象是否已经被初始化。
在4处,对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。

问题所在:
一切都是那么的美好,但是有一种情况,在线程执行到1处,读取到instance不为null时;在4处的线程正在初始化实例instance,但是instance引用的对象有可能还没有完成初始化,因为发生了指令重排。
4处因为指令重排,引发的1处拿到的实例在使用的时候发生空指针的问题。(拿到的对象是还未完成初始化的对象)


2.解决方式

2.1 基于volatile的解决方案

public class Singleton {
    
    private static volatile Singleton single = null;

    //静态工厂方法
    public static Singleton getInstance() {
        if (single == null) {							// 1处、第一次检查
        	// 只有当单例对象为空时, 才实例化对象(同步锁)
        	synchronized(Singleton.class) {				// 2处、加锁
        		if (single == null) {					// 3处、第二次检查
        			single = new Singleton();			// 4处、实例化对象【这里会出问题的】
        		}
        	}
        }
        return single;
    }
}

volatile 可以禁止single = new Singleton();过程中的指令重排,从而实现线程的安全。

项目中用的双重检查锁定是怎么回事

你可能感兴趣的:(单例模式,java,开发语言)