Java单例模式中双检锁的线程安全问题

一、 实现一个双检锁

双检锁,顾名思义,两次检查一次锁:

public class DoubleCheckLock {
    private static DoubleCheckLock instance;

    private DoubleCheckLock() {
    	// TODO
    }

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

二、 为什么线程不安全

当线程A执行到" instance = new DoubleCheckLock();"这一行,而线程B执行到外层"if (instance == null) "时,可能出现instance还未完成构造,但是此时不为null导致线程B获取到一个不完整的instance。

之所以会出现这种情况,要从JVM的指令重排序说起。


三、 关于指令重排序

指令重排序:是编译器在不改变执行效果的前提下,对指令顺序进行调整,从而提高执行效率的过程。
一个最简单的重排序例子:

int a = 1;
String b = "b";

对于这两行毫无关联的操作指令,编译器可能会将其顺序调整为:

String b = "b";
int a = 1;

此时该操作并不会影响后续指令的执行和执行结果。
再回过头看我们的双检锁内部,对于"instance = new DoubleCheckLock();"这一行代码,它分为三个步骤执行:

  • 1.分配一块内存空间
  • 2.在这块内存上初始化一个DoubleCheckLock的实例
  • 3.将声明的引用instance指向这块内存

第2和第3个步骤都依赖于第1个步骤,但是2和3之间没有依赖关系,那么如果编译器将2和3调换顺序,变成了:

  • 1.分配一块内存空间
  • 2.将声明的引用instance指向这块内存
  • 3.在这块内存上初始化一个DoubleCheckLock的实例

当线程A执行到第2步时,instance已经不为null了,因为它指向了这块内存,此时如果线程B走到了"if (instance == null)",那么线程B其实拿到的还是一个null,因为这块内存还没有初始化,这就出现了问题。

指令重排序是导致出现线程不安全的直接原因,而根本原因则是对象实例化不是一个原子操作。


四、 关于原子操作

原子操作:不可划分的最小单位操作,不会被线程调度机制打断,不会有线程切换,整个操作要么不执行,一旦执行就会运行到结束。

我们来看一个简单的例子:

Object a;
Object b = new Object();
a = b;

对于"a = b" 这一操作指令,将a这个引用指向b这一对象的内存,只需要改变a的指针,因此该直接赋值操作是一个不可划分的原子操作。

再看另一个例子:

int i = 0;
i ++;

对于"i ++"这一操作指令,其实它分为三个步骤执行:

  • 读取i的值
  • 将i的值加1
  • 将新的值赋值给i

类似的还有:

boolean b = true;
b = !b;

对于这些涉及自身值的操作,由于其最终实现需要划分更小的操作单位,因此均不是原子操作。

对于非原子操作,在多线程下就可能出现线程安全问题,这也是我们的双检锁不安全的根本原因,实例化对象不是一个原子操作。


五、 实现线程安全的双检锁

我们只需要对instance加上一个volatile修饰符便可解决线程安全问题,关于volatile的知识请阅读参考文献对应内容。

public class DoubleCheckLock {
    private static volatile DoubleCheckLock instance;

    private DoubleCheckLock() {
    	// TODO
    }

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

synchronized和volatile的完美配合,便实现了线程安全的双检锁单例模式。


六、 参考文献

发布了91 篇原创文章 · 获赞 39 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/zy13608089849/article/details/82703192
今日推荐