双重检查锁Double Checked Locking Pattern的非原子操作下的危险性

导读:本篇文章讲解 双重检查锁Double Checked Locking Pattern的非原子操作下的危险性,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

Double Checked Locking Pattern
即双重检查锁模式。

双重检查锁模式是一种软件设计模式,用于减少获取锁的开销。程序首先检查锁定条件,并且仅当检查表明需要锁时才才获取锁。

延迟初始化就是我们常说的懒加载是一种常用的策略,用于延迟对象初始化,直到它第一次被访问。在多线程环境中,初始化通常不是线程安全的,因此需要来保护临界区。由于只有第一次访问需要锁定,因此使用双重检查锁来避免后续访问的锁定开销。然而,在许多语言和硬件上,设计可能是不安全的。

Double-Checked Locking双重检查锁由来

在单线程中
如果我们正在编写单线程代码,我们可以像这样编写一个惰性初始化:
在这里插入图片描述
此代码适用于单个线程,但如果代码在多线程环境中运行,则两个或多个线程可能会同时找到它helper,null并创建对象的多个副本Helper。这甚至会导致某些语言(例如 C++)中的内存泄漏。

使用synchronized锁

为了解决这个问题,我们可以简单地给这个临界区加一个锁,如下所示,这样每次只有一个线程可以进入这个临界区。

类Foo {私人助手helper = null ;public Helper getHelper () {synchronized (this) {if (helper == null )helper = new Helper ();返回助手;}}}

但是,我们只需要为第一个线程访问而同步这部分代码。创建对象后,后面的线程就没有必要再次获取锁了。它们将对性能产生巨大影响。Always-synchronized solution is slow。
我们需要的是只有第一个线程会进入同步部分并创建对象。一旦helper初始化,所有后续访问都可以直接运行而无需同步。
直观地说,我们可以提出以下步骤来完成这项工作:

  • 在锁之前检查对象是否已经初始化。如果已经创建好了,则立即返回对象。
  • 获取锁之后再次检查对象是否已初始化。如果另一个线程之前已经抢到了锁,创建了对象,那么当前线程就可以看到对象被创建,并返回该对象。
  • 否则,当前线程将创建对象并返回。
    上述,我们将获得以下代码:
    在这里插入图片描述
    这种策略称为双重锁模式。

Double-checked locking is broken

但是helper = new Helper()不是原子操作,它由分配空间、初始化对象字段和分配地址的多条指令组成helper。
为了显示那里真正发生了什么,我们helper = new Helper()用一些伪代码进行扩展。
在这里插入图片描述

为了提高整体性能,一些编译器、内存系统或处理器可能会重新排序指令,我们之前文章《java多线程基础篇》中有提到了这种指令重排序
因为初始化字段helper = ptr;的指令和其它指令之间没有数据依赖关系。
所以重排序后helper = ptr可能就会提前执行;
那么其它线程可能会获取到一个null的helper对象。

原子操作

原子操作要么完全发生,要么根本不发生。没有中间状态,因此原子操作在完成操作之前是不可见的。

在前面的分析中,我们已经看到*h = new Helper()*可以交错,因为它不是原子操作。如果此操作是原子操作,则双重检查锁将起作用。

解决方案

1.使用volatile

从 JDK 5 开始,我们可以通过将任何变量声明为 volatile 变量来对任何变量进行原子读写。每次读取 volatile 都会使缓存值无效并从主内存中加载它。volatile 的每次写入都会更新缓存中的值,然后将缓存的值刷新到主内存,就是缓存一致性协议

Java 中的“volatile”还提供了排序保证,这与atomic_thread_fenceC++ 中提供的保证相同:

  • 从 volatile 变量读取后其他变量的读/写操作不能在从 volatile 变量读取之前重新排序。 在写入 volatile
  • 变量之前对其他变量的读/写操作不能在写入 volatile 变量之后重新排序。

有了这个新特性,双重检查锁定问题就可以通过简单地声明helper为 volatile 变量来解决。
在这里插入图片描述
但是,由于 volatile 变量的所有读写操作都会触发缓存一致性协议并访问主存,因此可能会非常慢。可以使用局部变量进行改进,以减少访问 volatile 变量的次数。
在这里插入图片描述
Oracle Java文档:Atomic Access原子访问long指定大多数原始变量(除了并且double因为它们是 64 位)的读写操作是原子的。

静态单例

如果helper是静态的,即类的所有实例Foo共享同一个实例,在单独的类的静态字段中helper定义就可以解决问题。

双重检查锁Double Checked Locking Pattern的非原子操作下的危险性

这被称为Initialization on Demand Holder(IoDH 持有者按需初始化),它被认为是所有 Java 版本的安全高效的并发延迟初始化。

在软件开发中,Initialization on Demand Holder设计模式描述了所谓的惰性初始化单例的实现选项,即对象仅在第一次使用时才被初始化的实现。在所有 Java 版本中,它允许安全、高度可并行化的延迟初始化,并具有良好的性能。

使用ThreadLocal

Alexander Terekhov 提供了使用线程局部变量的双重检查锁定的实现。
Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。
ThreadLocal实例通常总是以静态字段初始化如下:

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

ThreadLocal可以用来维护“状态是否经过同步初始化”的状态。如果一个线程已经完成了一次同步初始化,它可以确信该对象已经被初始化。

在同步初始化部分中,只有第一个线程会找到null对象并初始化对象。然后所有线程将在第一次同步访问时更改其每个线程的状态,以便它们不会再去获取锁进入同步部分。
在这里插入图片描述

廖雪峰 关于ThreadLocal更加详细的
结论
文章讨论了多线程环境下延迟初始化的双重检查锁定问题。它分析了为什么一些直观的解决方案不起作用,并分析了一些可行的解决方案。

编写多线程程序很难。编写正确且安全的多线程程序更加困难。在分析多线程程序的正确性时,需要考虑多个组件,包括编译器、系统和处理器。另一方面,在设计编译器、系统或处理器时,还需要考虑常用的设计模式。

本文主要来自
Double-Checked Locking is Brokenby Hongbo Zhang October 18, 2019
Java多线程基础(二)——Java内存模型 Ressmix

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/63418.html

(0)
小半的头像小半

相关推荐

半码博客——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!