Java内存模型之从JMM角度分析DCL

本文深入剖析了DCL(Double Check Locking)在单例模式中的应用与问题,揭示了其潜在缺陷并提出了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

DCL,即Double Check Lock,中卫双重检查锁定。其实DCL很多人在单例模式中用过,LZ面试人的时候也要他们写过,但是有很多人都会写错。他们为什么会写错呢?其错误根源在哪里?有什么解决方案?下面就随LZ一起来分析

问题分析

我们先看单例模式里面的懒汉式:

publicclassSingleton{privatestaticSingleton singleton;privateSingleton(){}publicstaticSingleton getInstance(){if(singleton ==null){
            singleton =newSingleton();}return singleton;}}

我们都知道这种写法是错误的,因为它无法保证线程的安全性。优化如下:

publicclassSingleton{privatestaticSingleton singleton;privateSingleton(){}publicstaticsynchronizedSingleton getInstance(){if(singleton ==null){
            singleton =newSingleton();}return singleton;}}

优化非常简单,就是在getInstance方法上面做了同步,但是synchronized就会导致这个方法比较低效,导致程序性能下降,那么怎么解决呢?聪明的人们想到了双重检查 DCL:

publicclassSingleton{privatestaticSingleton singleton;privateSingleton(){}publicstaticSingleton getInstance(){if(singleton ==null){// 1synchronized(Singleton.class){// 2if(singleton ==null){// 3
                    singleton =newSingleton();// 4}}}return singleton;}}

就如上面所示,这个代码看起来很完美,理由如下:

  • 如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能;
  • 如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象;
  • 当第一个获取锁的线程创建完成后singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象;

通过上面的分析,DCL看起确实是非常完美,但是可以明确地告诉你,这个错误的。上面的逻辑确实是没有问题,分析也对,但是就是有问题,那么问题出在哪里呢?在回答这个问题之前,我们先来复习一下创建对象过程,实例化一个对象要分为三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将内存空间的地址赋值给对应的引用

但是由于重排序的缘故,步骤2、3可能会发生重排序,其过程如下:

  1. 分配内存空间
  2. 将内存空间的地址赋值给对应的引用
  3. 初始化对象

如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象,如下:

DCL00001_2

按照上面图例所示,线程B访问的是一个没有被初始化的singleton对象。

通过上面的阐述,我们可以判断DCL的错误根源在于步骤4:

singleton =newSingleton();

知道问题根源所在,那么怎么解决呢?有两个解决办法:

  1. 不允许初始化阶段步骤2 、3发生重排序。
  2. 允许初始化阶段步骤2 、3发生重排序,但是不允许其他线程“看到”这个重排序。

解决方案

解决方案依据上面两个解决办法即可。

基于volatile解决方案

对于上面的DCL其实只需要做一点点修改即可:将变量singleton生命为volatile即可:

publicclassSingleton{//通过volatile关键字来确保安全privatevolatilestaticSingleton singleton;privateSingleton(){}publicstaticSingleton getInstance(){if(singleton ==null){synchronized(Singleton.class){if(singleton ==null){
                    singleton =newSingleton();}}}return singleton;}}

当singleton声明为volatile后,步骤2、步骤3就不会被重排序了,也就可以解决上面那问题了。

基于类初始化的解决方案

该解决方案的根本就在于:利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化

publicclassSingleton{privatestaticclassSingletonHolder{publicstaticSingleton singleton =newSingleton();}publicstaticSingleton getInstance(){returnSingletonHolder.singleton;}}

这种解决方案的实质是:运行步骤2和步骤3重排序,但是不允许其他线程看见。

Java语言规定,对于每一个类或者接口C,都有一个唯一的初始化锁LC与之相对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。

singleton

 

 

 

http://cmsblogs.com/?p=2161

<think>嗯,用户让我详细介绍Java内存模型JMM),我得先回忆一下相关知识。Java内存模型主要跟多线程编程有关,对吧?用户之前已经问过多线程与并发的问题,现在可能想深入理解底层的原理。他们可能在学习多线程时遇到了内存可见性或者同步的问题,所以需要了解JMM如何解决这些问题。 首先,我需要确定JMM的定义和作用。JMMJava虚拟机规范的一部分,用来定义线程如何与主内存交互,确保在不同平台上的内存访问一致性。然后要讲清楚JMM的三大特性:原子性、可见性、有序性。用户可能对这些概念有点模糊,需要用例子说明。 接下来是主内存和工作内存的概念。主内存是所有线程共享的,而每个线程有自己的工作内存,存储变量的副本。这里需要解释线程如何从主内存读取数据到工作内存,以及如何写回,这可能涉及到volatile关键字的作用。 然后要讲happens-before原则,这是JMM的核心规则,用来确定操作之间的可见性。需要列出几个重要的happens-before规则,比如程序顺序规则、volatile变量规则、锁规则等,并举例说明它们如何保证可见性。 内存屏障也是一个关键点,但用户可能对底层实现不太熟悉,所以需要简化解释,说明它是如何禁止指令重排序的。volatile的实现原理需要详细说明,包括读写操作的内存屏障插入位置,以及如何保证可见性和有序性。 锁的同步机制和JMM的关系也很重要,要说明synchronized和Lock如何保证原子性和可见性,可能和happens-before规则中的锁规则联系起来。原子类比如AtomicInteger,它们的实现基于CAS,这里需要提到CAS的操作和底层CPU指令,比如cmpxchg。 最后,实际应用中的问题,比如单例模式的双重检查锁问题,需要结合volatile来避免指令重排序导致的错误。这部分通过例子来说明JMM的重要性,以及如何正确应用知识来解决实际问题。 用户可能的疑问点在于指令重排序和内存屏障的具体机制,是否需要深入底层?但考虑到用户可能是中级开发者,应该保持解释在适度的技术层面,避免过于复杂的细节。同时,需要检查是否有遗漏的重要概念,比如final字段的内存语义,或者JMM与硬件内存模型的关系,但可能这些内容对于概述来说太深入,可以适当提到但不展开。 总结一下,结构应该是先定义JMM,再讲核心概念,接着是happens-before规则,然后内存屏障和volatile实现,最后实际应用案例。这样用户能由浅入深地理解JMM的作用和重要性,同时结合实际问题来巩固知识。</think>以下是Java内存模型JMM, Java Memory Model)的系统解析,结合其设计原理、核心规则和实际影响展开说明: --- ### 一、JMM的定义与目标 **核心作用**:规范多线程环境下,线程如何通过内存交互实现**可见性**、**有序性**和**原子性**,屏蔽不同硬件/操作系统内存访问差异,为开发者提供一致的内存语义保证。 --- ### 二、核心概念 #### 1. **主内存与工作内存** - **主内存(Main Memory)**:所有线程共享的内存区域,存储变量原始值 - **工作内存(Working Memory)**:每个线程私有的内存副本,存储线程操作所需变量的拷贝 $$ \text{线程操作流程}:\text{主内存} \rightleftharpoons \text{工作内存} \rightarrow \text{CPU寄存器} $$ #### 2. **内存间交互操作** - **8种原子操作**(Java虚拟机规范定义): ```markdown - lock/unlock(锁定主内存变量) - read/load(主内存 → 工作内存) - use/assign(工作内存使用/赋值) - store/write(工作内存 → 主内存) ``` --- ### 三、关键特性保障 #### 1. **原子性(Atomicity)** - JMM直接保证的原子操作:`read`/`load`/`use`/`assign`/`store`/`write` - **扩展原子性**:通过`synchronized`/`Lock`/`原子类`实现代码块级原子操作 #### 2. **可见性(Visibility)** - **实现机制**: - `volatile`变量:强制读写直接穿透缓存到主内存 - `synchronized`:解锁前将变量同步回主内存 - `final`字段:构造器完成后的可见性保证 #### 3. **有序性(Ordering)** - **指令重排序问题**:编译器/处理器优化可能打乱代码执行顺序 - **控制手段**: - `volatile`禁止重排序(内存屏障) - `happens-before`规则约束 --- ### 四、happens-before规则 #### 1. **定义** 若操作A **happens-before** 操作B,则A的结果对B可见,且A的执行顺序排在B之前。 #### 2. **核心规则列表** ```markdown - **程序顺序规则**:单线程中的顺序执行 - **volatile规则**:volatile写happens-before后续读 - **锁规则**:解锁happens-before后续加锁 - **线程启动规则**:Thread.start()前的操作happens-before新线程所有操作 - **传递性**:若A→B且B→C,则A→C ``` #### 3. **示例说明** ```java // volatile写操作happens-before读操作 volatile boolean flag = false; // 线程1 flag = true; // 写操作(happens-before后续读) // 线程2 if (flag) { // 读操作能看到线程1的修改 System.out.println("Flag is true"); } ``` --- ### 五、内存屏障(Memory Barrier) #### 1. **作用** 禁止特定类型的指令重排序,强制刷新内存数据 #### 2. **JVM实现类型** | 屏障类型 | 作用 | |--------------------|-----------------------------| | LoadLoad屏障 | 禁止读操作重排序 | | StoreStore屏障 | 禁止写操作重排序 | | LoadStore屏障 | 禁止读后写重排序 | | StoreLoad屏障 | 禁止写后读重排序(开销最大) | #### 3. **volatile实现原理** ```markdown - **写操作**:插入StoreStore + StoreLoad屏障 - **读操作**:插入LoadLoad + LoadStore屏障 ``` --- ### 六、JMM与并发编程 #### 1. **双重检查锁定(DCL)问题** ```java // 错误实现(未加volatile) class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 可能发生指令重排序 } } } return instance; } } // 正确实现:添加volatile修饰符 private static volatile Singleton instance; ``` #### 2. **final字段的特殊处理** - 构造函数内对final字段的写入,happens-before任何其他线程读取该对象引用 --- ### 七、JMM与硬件内存模型 1. **关系映射** - JMM抽象层:定义程序员可见的保证 - 硬件实现差异:x86(强内存模型) vs. ARM(弱内存模型) - JVM通过插入内存屏障适配不同硬件 2. **缓存一致性协议** - MESI协议:CPU缓存行状态管理(Modified/Exclusive/Shared/Invalid) --- ### 八、诊断工具 1. **JMM验证工具**:JCStress(测试并发行为) 2. **内存分析工具**:Eclipse Memory Analyzer 3. **调试命令**:`jconsole`查看线程内存状态 --- 通过理解JMM,开发者可以更精准地编写线程安全代码,避免因内存可见性、指令重排序等导致的隐蔽并发问题。实际开发中应优先使用`java.util.concurrent`包提供的高层抽象工具,而非直接操作底层内存模型
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值