双重检查锁Double Checked Locking Pattern的非原子操作下的危险性
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的非原子操作下的危险性相关推荐
- 双重检查锁(Double-Checked Locking)的缺陷
双重检查锁(Double-Checked Locking)的缺陷 第一种有问题的写法 第二种有问题的写法 第三种有问题的写法 它不起作用 它不起作用的第一个原因 一个测试用例显示它不起作用 一个不起作 ...
- java 双重检查锁 有序_Java中的双重检查锁(double checked locking)
1 public classSingleton {2 private staticSingleton uniqueSingleton;3 4 privateSingleton() {5 }6 7 pu ...
- Java中的双重检查锁(double checked locking)
起因 在实现单例模式时,如果未考虑多线程的情况,很容易写出下面的代码(也不能说是错误的): public class Singleton {private static Singleton uniqu ...
- 单例模式之双重检查锁(double check locking)的发展历程
不安全的单例 没有注意过多线程安全问题的时候,我们的单例可能是这样的: public final class Singleton {private static Singleton instance; ...
- 单例模式,懒汉饿汉,线程安全,double checked locking的问题
概览 本文目的 单例 饿汉模式 懒汉模式 线程安全的Singleton实现 懒汉普通加锁 double checked locking double checked locking 靠不住? 静态局部 ...
- java双重检查锁单例真的线程安全吗?
相信大多数同学在面试当中都遇到过手写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的单例模式说不定就直接决定了面试结果,今天我们就要来讲讲看似线程安全的双重检查锁单例模 ...
- 双重检查锁模式导致空指针
今天遇到一个问题:莫名奇妙报了个空指针,后来发现原来单例模式在高并发下引起的: 双重检查锁模式的一般实现: 双重检查锁模式解决了单例.性能.线程安全问题,但是这种写法同样存在问题:在多线程的情况下,可 ...
- java 双重检查锁_Java中可怕的双重检查锁定习惯用法
java 双重检查锁 本文讨论的问题不是新问题,但即使是经验丰富的开发人员也仍然很棘手. 单例模式是常见的编程习惯用法. 但是,当与多个线程一起使用时,必须进行某种类型的同步,以免破坏代码. 在相关文 ...
- 双重检查锁,原来是这样演变来的,你了解吗
最近在看Nacos的源代码时,发现多处都使用了"双重检查锁"的机制,算是非常好的实践案例.这篇文章就着案例来分析一下双重检查锁的使用以及优势所在,目的就是让你的代码格调更加高一个层 ...
最新文章
- php常用插件,关于PHP网站编程中常用插件的使用——w3cdream|前端学习-开发
- 如何零基础或者转行数据分析师?
- 《SpringBoot揭秘 快速构建微服务体系》读后感(三)
- LeetCode之Construct the Rectangle
- OpenStack nova-network 支持多vlan技术实现片段代码
- 【软件工程】实体类的持久性
- 支付宝有50万存款,但欠30万房贷。是还房贷好,还是买基金好?
- [转载]Qt之模型/视图(自定义风格)
- java 双击触发事件,用RxJava2的方式实现点击事件
- 软件需求规格说明书范例
- error: L6002U: Could not open file .\objects\startup_stm32f10x_hd.o
- 有人在研究arroundme 吗,一个开源的php社会化网络程序
- uniapp 日期时间 计算
- 英语演讲常用连接词和句子
- javascript原生实现二级联动下拉菜单
- ajax传递数组参数
- 廖雪峰官方网站python学习笔记:第一个Pyhon程序
- 7-3 出租车计价 (15分)
- Boyd Corporation宣布其南亚工厂获得ISO 13485:2016认证
- GPS时间转化成标准时间格式
热门文章
- Python关键字及其含义
- 相似向量检索库-Faiss-简介及原理
- 我的周刊(第037期)
- BI、数据仓库、ETL、大数据开发工程师有什么区别?
- 联想开机启动项按哪个_联想笔记本开机按F2进 BIOS BOOT启动选项,找不到u盘启动项,怎么设定...
- Dockerfile的ADD和COPY命令
- traditional:true
- JVM(类加载、运行时数据区、堆内存、方法区、本地接口、执行引擎和垃圾回收)java虚拟机(JVM)的超详细知识点
- 1.4CAD2017绘图基础
- html画倒金字塔直线,HTML5/Canvas 生成金字塔动画