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

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

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

Double-Checked Locking双重检查锁由来

在单线程中
如果我们正在编写单线程代码,我们可以像这样编写一个惰性初始化:

此代码适用于单个线程,但如果代码在多线程环境中运行,则两个或多个线程可能会同时找到它helper,null并创建对象的多个副本Helper。这甚至会导致某些语言(例如 C++)中的内存泄漏。

使用synchronized锁

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

但是,我们只需要为第一个线程访问而同步这部分代码。创建对象后,后面的线程就没有必要再次获取锁了。它们将对性能产生巨大影响。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定义就可以解决问题。

这被称为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

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

  1. 双重检查锁(Double-Checked Locking)的缺陷

    双重检查锁(Double-Checked Locking)的缺陷 第一种有问题的写法 第二种有问题的写法 第三种有问题的写法 它不起作用 它不起作用的第一个原因 一个测试用例显示它不起作用 一个不起作 ...

  2. java 双重检查锁 有序_Java中的双重检查锁(double checked locking)

    1 public classSingleton {2 private staticSingleton uniqueSingleton;3 4 privateSingleton() {5 }6 7 pu ...

  3. Java中的双重检查锁(double checked locking)

    起因 在实现单例模式时,如果未考虑多线程的情况,很容易写出下面的代码(也不能说是错误的): public class Singleton {private static Singleton uniqu ...

  4. 单例模式之双重检查锁(double check locking)的发展历程

    不安全的单例 没有注意过多线程安全问题的时候,我们的单例可能是这样的: public final class Singleton {private static Singleton instance; ...

  5. 单例模式,懒汉饿汉,线程安全,double checked locking的问题

    概览 本文目的 单例 饿汉模式 懒汉模式 线程安全的Singleton实现 懒汉普通加锁 double checked locking double checked locking 靠不住? 静态局部 ...

  6. java双重检查锁单例真的线程安全吗?

     相信大多数同学在面试当中都遇到过手写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的单例模式说不定就直接决定了面试结果,今天我们就要来讲讲看似线程安全的双重检查锁单例模 ...

  7. 双重检查锁模式导致空指针

    今天遇到一个问题:莫名奇妙报了个空指针,后来发现原来单例模式在高并发下引起的: 双重检查锁模式的一般实现: 双重检查锁模式解决了单例.性能.线程安全问题,但是这种写法同样存在问题:在多线程的情况下,可 ...

  8. java 双重检查锁_Java中可怕的双重检查锁定习惯用法

    java 双重检查锁 本文讨论的问题不是新问题,但即使是经验丰富的开发人员也仍然很棘手. 单例模式是常见的编程习惯用法. 但是,当与多个线程一起使用时,必须进行某种类型的同步,以免破坏代码. 在相关文 ...

  9. 双重检查锁,原来是这样演变来的,你了解吗

    最近在看Nacos的源代码时,发现多处都使用了"双重检查锁"的机制,算是非常好的实践案例.这篇文章就着案例来分析一下双重检查锁的使用以及优势所在,目的就是让你的代码格调更加高一个层 ...

最新文章

  1. php常用插件,关于PHP网站编程中常用插件的使用——w3cdream|前端学习-开发
  2. 如何零基础或者转行数据分析师?
  3. 《SpringBoot揭秘 快速构建微服务体系》读后感(三)
  4. LeetCode之Construct the Rectangle
  5. OpenStack nova-network 支持多vlan技术实现片段代码
  6. 【软件工程】实体类的持久性
  7. 支付宝有50万存款,但欠30万房贷。是还房贷好,还是买基金好?
  8. [转载]Qt之模型/视图(自定义风格)
  9. java 双击触发事件,用RxJava2的方式实现点击事件
  10. 软件需求规格说明书范例
  11. error: L6002U: Could not open file .\objects\startup_stm32f10x_hd.o
  12. 有人在研究arroundme 吗,一个开源的php社会化网络程序
  13. uniapp 日期时间 计算
  14. 英语演讲常用连接词和句子
  15. javascript原生实现二级联动下拉菜单
  16. ajax传递数组参数
  17. 廖雪峰官方网站python学习笔记:第一个Pyhon程序
  18. 7-3 出租车计价 (15分)
  19. Boyd Corporation宣布其南亚工厂获得ISO 13485:2016认证
  20. GPS时间转化成标准时间格式

热门文章

  1. Python关键字及其含义
  2. 相似向量检索库-Faiss-简介及原理
  3. 我的周刊(第037期)
  4. BI、数据仓库、ETL、大数据开发工程师有什么区别?
  5. 联想开机启动项按哪个_联想笔记本开机按F2进 BIOS BOOT启动选项,找不到u盘启动项,怎么设定...
  6. Dockerfile的ADD和COPY命令
  7. traditional:true
  8. JVM(类加载、运行时数据区、堆内存、方法区、本地接口、执行引擎和垃圾回收)java虚拟机(JVM)的超详细知识点
  9. 1.4CAD2017绘图基础
  10. html画倒金字塔直线,HTML5/Canvas 生成金字塔动画