`
m635674608
  • 浏览: 5039866 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

入分析volatile的实现原理

    博客分类:
  • java
 
阅读更多

通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized。如果一个变量使用volatile,则它比使用synchronized的成本更加低,因为它不会引起线程上下文的切换和调度。Java语言规范对volatile的定义如下:

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

上面比较绕口,通俗点讲就是说一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。

volatile虽然看起来比较简单,使用起来无非就是在一个变量前面加上volatile即可,但是要用好并不容易(LZ承认我至今仍然使用不好,在使用时仍然是模棱两可)。

内存模型相关概念

理解volatile其实还是有点儿难度的,它与Java的内存模型有关,所以在理解volatile之前我们需要先了解有关Java内存模型的概念,这里只做初步的介绍,后续LZ会详细介绍Java内存模型。

操作系统语义

计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。

有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。举一个简单的例子:

i++i++

当线程运行这段代码时,首先会从主存中读取i( i = 1),然后复制一份到CPU高速缓存中,然后CPU执行 + 1 (2)的操作,然后将数据(2)写入到告诉缓存中,最后刷新到主存中。其实这样做在单线程中是没有问题的,有问题的是在多线程中。如下:

假如有两个线程A、B都执行这个操作(i++),按照我们正常的逻辑思维主存中的i值应该=3,但事实是这样么?分析如下:

两个线程从主存中读取i的值(1)到各自的高速缓存中,然后线程A执行+1操作并将结果写入高速缓存中,最后写入主存中,此时主存i==2,线程B做同样的操作,主存中的i仍然=2。所以最终结果为2并不是3。这种现象就是缓存一致性问题。

解决缓存一致性方案有两种:

  1. 通过在总线加LOCK#锁的方式
  2. 通过缓存一致性协议

但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。

第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。

212219343783699

Java内存模型

上面从操作系统层次阐述了如何保证数据一致性,下面我们来看一下Java内存模型,稍微研究一下Java内存模型为我们提供了哪些保证以及在Java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性。

在并发编程中我们一般都会遇到这三个基本概念:原子性、可见性、有序性。我们稍微看下volatile

原子性

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性就像数据库里面的事务一样,他们是一个团队,同生共死。其实理解原子性非常简单,我们看下面一个简单的例子即可:

i =0;---1
j = i ;---2
i++;---3
i = j +1;---4

上面四个操作,有哪个几个是原子操作,那几个不是?如果不是很理解,可能会认为都是原子性操作,其实只有1才是原子操作,其余均不是。

1—在Java中,对基本数据类型的变量和赋值操作都是原子性操作; 
2—包含了两个操作:读取i,将i值赋值给j 
3—包含了三个操作:读取i值、i + 1 、将+1结果赋值给i; 
4—同三一样

在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作*,如long、double)。要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。

volatile是无法保证复合操作的原子性

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在上面已经分析了,在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。

Java提供了volatile来保证可见性。

当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。 
当然,synchronize和锁都可以保证可见性。

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。

Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。这里LZ就不再阐述了。

剖析volatile原理

JMM比较庞大,不是上面一点点就能够阐述的。上面简单地介绍都是为了volatile做铺垫的。

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。

上面那段话,有两层语义

  1. 保证可见性、不保证原子性
  2. 禁止指令重排序

第一层语义就不做介绍了,下面重点介绍指令重排序。

在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:

  1. 编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
  2. 处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

指令重排序对单线程没有什么影响,他不会影响程序的运行结果,但是会影响多线程的正确性。既然指令重排序会影响到多线程执行的正确性,那么我们就需要禁止重排序。那么JVM是如何禁止重排序的呢?这个问题稍后回答,我们先看另一个原则happens-before,happen-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before原则中推到出来,那么他们就不能保证有序性,可以随意进行重排序。其定义如下:

  1. 同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。
  2. 监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)
  3. 对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)
  4. 线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)
  5. 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
  6. 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

我们着重看第三点volatile规则:对volatile变量的写操作 happen-before 后续的读操作。为了实现volatile内存语义,JMM会重排序,其规则如下:

对happen-before原则有了稍微的了解,我们再来回答这个问题JVM是如何禁止重排序的?

20170104-volatile

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。下图是完成上述规则所需要的内存屏障:

volatile暂且下分析到这里,JMM体系较为庞大,不是三言两语能够说清楚的,后面会结合JMM再一次对volatile深入分析。

20170104-volatile2

总结

volatile看起来简单,但是要想理解它还是比较难的,这里只是对其进行基本的了解。volatile相对于synchronized稍微轻量些,在某些场合它可以替代synchronized,但是又不能完全取代synchronized,只有在某些场合才能够使用volatile。使用它必须满足如下两个条件:

  1. 对变量的写操作不依赖当前值;
  2. 该变量没有包含在具有其他变量的不变式中。

volatile经常用于两个两个场景:状态标记两、double check

参考资料

  1. 周志明:《深入理解Java虚拟机》
  2. 方腾飞:《Java并发编程的艺术》
  3. Java并发编程:volatile关键字解析
  4. Java 并发编程:volatile的使用及其原理

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

分享到:
评论

相关推荐

    深入分析Volatile的实现原理

    本文将深入分析在硬件层面上Inter处理器是如何实现Volatile的,通过深入分析能帮助我们正确的使用Volatile变量。

    聊聊并发(一)深入分析Volatile的实现原理

    本篇文章将深入分析Volatile的实现原理,结合`LinkedTransferQueue`和`TransferQueue`这两个与并发相关的Java源码,探讨其在多线程环境中的应用。 首先,我们需要理解Java内存模型(JMM,Java Memory Model),它是...

    synchronized ReentrantLock volatile Atomic 原理分析.docx

    本文将深入探讨四种关键的并发控制机制:synchronized关键字、ReentrantLock(可重入锁)、volatile关键字以及Atomic类的原理与应用。 ### 1. synchronized关键字 `synchronized`关键字是Java提供的内置锁,用于...

    volatile源码分析1

    【volatile源码分析1】 Java中的volatile关键字是一个关键的同步机制,它在多线程编程中扮演着重要的角色。在面试和技术讨论中,volatile经常成为焦点,但其工作原理却常常引发争议。本文将从JVM、C++以及汇编语言...

    简单了解java volatile关键字实现的原理

    一、`volatile`关键字的语义分析 1. **保证可见性**:当一个线程修改了`volatile`变量的值,其他所有线程都能立即看到这个变化。这是因为`volatile`变量的写操作会强制将当前处理器的缓存行写回主内存,其他处理器...

    java 中volatile和lock原理分析

    《Java中volatile与锁原理分析》 在多线程编程中,确保数据的一致性和可见性是至关重要的。Java提供了两种主要的机制来实现这一目标:volatile关键字和锁。本文将深入探讨这两种机制的原理,以及它们在实际编程中的...

    volatile详解

    #### 二、volatile的工作原理 在默认情况下,编译器为了提高执行效率,往往会采用各种优化手段。其中一种常见的做法是在处理变量时,先将变量的值加载到CPU寄存器中,随后的操作就直接针对寄存器进行,而不是每次都...

    深入分析java并发编程中volatile的实现原理

    `volatile`的实现原理涉及到处理器层面的内存模型。在x86架构的处理器中,对`volatile`变量的写操作会额外添加一个`lock`前缀的汇编指令。这个指令有两个关键作用: 1. 将当前处理器缓存行的数据写回到系统内存,...

    volatile使用详解

    - **内存模型**:Java 或 C++ 等语言都有自己的内存模型,`volatile` 关键字的实现依赖于这些内存模型。例如,在 Java 中,`volatile` 变量能够确保可见性,即当一个线程修改了 `volatile` 变量后,其他线程能立即...

    c语言词法分析器和c语言语法分析器编译原理课程设计论文正文--大学毕业论文设计.doc

    编译原理课程设计之C语言词法分析器和C语言语法分析器 在计算机科学中,编译原理是指将高级语言代码翻译成机器语言代码的过程。编译原理课程设计是计算机专业学生的必修课,旨在帮助学生掌握编译原理的基本概念和...

    嵌入式操作系统FreeRTOS的原理与实现

    通过对FreeRTOS的研究,我们可以深入理解嵌入式操作系统的实现原理。本文将详细介绍FreeRTOS中的任务调度机制、时间管理机制、任务管理机制以及内存分配策略,并分析其在实际应用中的优点和不足。 #### 1. FreeRTOS...

    编译原理-词法分析

    ### 编译原理-词法分析 #### 一、词法分析概述 词法分析是编译过程中的第一个阶段,其主要任务是对源程序进行扫描并...词法分析器的设计与实现对于理解编译原理至关重要,同时也是深入学习编程语言基础的重要环节。

    编译原理实验-词法分析器

    ### 编译原理实验——词法分析器 #### 背景介绍 词法分析是编译过程中的第一步,其主要任务是从源程序字符流中识别出一个个具有独立意义的单词符号(Token)。本实验旨在设计并实现一个针对C语言的简单词法分析器。...

    编译原理 厦大 实验一

    本实验的目的是通过实现词法分析器,更加深入地掌握词法分析的原理和技术。在编译原理课程中,词法分析是编译过程的第一阶段,它将源代码分解成一个个Token,以便后续的语法分析和语义分析阶段。本实验要求使用C++...

    ConcurrentHashMap底层实现机制的分析1

    在本文中,我们将深入探索 ConcurrentHashMap 的高并发实现机制,并分析其在 Java 内存模型基础上的实现原理。了解 ConcurrentHashMap 的实现机制有助于我们更好地理解 Java 并发编程的原理和技术。 一、Java 内存...

    聊聊并发系列文章

    通过对Volatile实现原理的深入了解,我们能够更好地掌握如何在实际应用中正确使用它。Volatile虽然提供了一种轻量级的同步机制,但它并不是万能的解决方案。开发人员需要仔细评估应用场景,确定是否适合使用Volatile...

    深入JVM内核—原理、诊断与优化

    本教程会讲解各种垃圾收集器(如Serial、Parallel、CMS、G1、ZGC、Shenandoah等)的工作原理和调优策略,以及如何分析内存泄漏和对象存活周期。 4. **性能监控与诊断工具**:JDK提供了一系列强大的工具,如JConsole...

    内嵌汇编用法及分析 asm

    ### 内嵌汇编用法及分析 #### 引言 在现代软件开发过程中,内嵌汇编(Inline Assembly)是一种特殊的技术手段,允许开发者直接在高级语言(如C/C++)代码中嵌入汇编指令。这种方法可以提高程序性能、优化特定功能...

    VLSI-Design of Non-Volatile Memories

    - **非易失性存储器技术**:详细分析了不同类型的非易失性存储器的工作原理、结构特点以及优缺点。 - **NOR Flash 的设计与实现**:特别关注了NOR Flash的设计方法、制造工艺和性能优化技巧。 - **案例研究**:提供...

    深入JVM内核 - 原理、诊断与优化

    内存模型和volatile实例 解释和编译运行的概念 介绍JVM的内部结构、启动流程以及内存模型。并介绍JVM字节码的执行方式。 第三课 常用JVM参数 堆的分配参数 栈分配及实例讲解 server与client模式 调试跟踪参数 介绍...

Global site tag (gtag.js) - Google Analytics