由volatile关键字谈Java内存模型

volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景

1. 内存模型的相关概念

当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中(read–>update–>set,应用程序,缓存,数据库修改内容都遵循这样一种模式),比如最常见的:

i += 1;

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中.

这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了,因为每个线程的缓存中都存在这个变量的副本,当两个线程都对这个变量进行修改的时候, 就会产生“覆盖”的问题.

比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?

最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量

为了解决缓存不一致性问题,通常来说有以下2种解决方法:

  1.通过在总线加LOCK#锁的方式

  2.通过缓存一致性协议

  这2种方式都是硬件层面上提供的方式

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的

这样做的弊端也显而易见: 在锁住总线期间,其他CPU无法访问内存,导致效率低下

缓存一致性协议,它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号 通知其他CPU将该变量的缓存行置为无效状态 ,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取

2. 并发编程的三个概念

1.原子性: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行(对比数据库事务中的atom属性)

典型的数据库中的例子就是支付宝转账,A账户转账到B账户。

同样在并发编程中,并发的过程需要是原子的。 试想一下当一个线程中的赋值过程进行到中间状态被其他线程读取, 会是什么结果。

2.可见性, 当一个线程修改了共享变量了过后,其他线程能够看见修改后的值。

//线程1执行的代码
int i = 0;
i = 10;

//线程2执行的代码
j = i;

这就可能存在可见性问题,线程1对变量i修改为10之后,线程2没有立即看到线程1的修改,可能j取到的值还是0.

3.有序性: 程序执行按照代码先后顺序执行

int i = 0;
boolean flag = false;
i = 1;                //语句1
flag = true;          //语句2这里写代码片

语句1的执行顺序不一定在语句2之前,编译器和cpu为了提高程序运行效率,可能回对代码进行优化,可能会执行指令重排(instruction reorder)

指令是否重排主要考虑的是指令之间的数据依赖性,看下面的例子:

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

语句3对语句1有数据依赖,则语句3的执行顺序一定是在语句1之后

语句1和语句2 的执行顺序则不确定。

3. java内存模型

Java内存模型为我们提供了哪些保证以及在java中提供了哪些方法和机制来让我们在进行多线程编程时能够保证程序执行的正确性

在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

java内存模型定义了程序中变量的访问规则,也就是定义了程序执行的次序

Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存

例如执行: i = 10; 执行线程必须先在自己的工作内存中进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中

  1. 原子性

    在java内存操作中,只有基本的赋值和读取操作是原子操作,其他类型都是非原子操作

x = 10;         //原子
y = x;         //先读取x,再赋值给y
x++;           //先读取x,再x+1,然后更新主内存
x = x + 1;     //同上

jvm对64位的long类型和double类型的赋值已经处理为原子操作,不用再纠结此问题

针对更大范围的原子操作,只能通过互斥同步 的方式来实现,在java中就是通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性

2.可见性 —> 内容修改后,会及时由工作内存刷到主内存

a. 针对单个变量, 使用volatile关键字来保证可见性,当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值

b. JUC中的atomic类型变量也具备volatile的特性

c. 通过synchronized和Lock也能够保证可见性, 这是因为 在释放锁之前会将对变量的修改刷新到主存当中 。因此可以保证可见性

3.有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

happens-before原则:

1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

2.锁定规则:一个unLock操作先行发生于后面对同一个锁lock操作

3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

4.传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

5.线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

其中,前面四条,是比较关键的四条规则

<>注意,两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行, happens-before仅仅要求前一个操作(执行的结果)对后一个操作是可见的.


4.回到volatile

字面意思,不稳定的, 意味着被其修饰的变量是易变的

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存, 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量

  2)对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

volatile能在一定程度上保证有序性

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行

x = 2;        //语句1
y = 0;        //语句2
volatile flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5

在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面

所以执行到语句3时,语句3前面的语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的

volatile的实现原理

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

  2)它会强制将对缓存的修改操作立即写入主存;

  3)如果是写操作,它会导致其他CPU中对应的缓存行无效



部分内容引用自并发编程网, 《深入理解java虚拟机》,涉及侵权请联系本人删除

时间: 2024-10-10 04:09:12

由volatile关键字谈Java内存模型的相关文章

浅谈Java内存模型

Java内存模型虽说是一个老生常谈的问题 ,也是大厂面试中绕不过的,甚至初级面试也会问到.但是真正要理解起来,还是相当困难,主要这个东西看不见,摸不着.网上已经有大量的博客,但是人家的终究是人家的,自己也要好好的去理解,去消化.今天我也来班门弄斧,说下Java内存模型. 说到Java内存模型,不得不说到 计算机硬件方面的知识. 计算机硬件体系 我们都知道CPU 和 内存是计算机中比较核心的两个东西,它们之间会频繁的交互,随着CPU发展越来越快,内存的读写的速度远远不如CPU的处理速度,所以CPU

对java内存模型的认识

浅谈java内存模型        不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的.其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改.总结java的内存模型,要解决两个主要的问题:可见性和有序性.我们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的.JVM定义了自己的内存模型,屏蔽了底层平台内存管理细节,对于java开发人员,要清楚在jvm内存模型的基础上,如果解决多线程的可见性和有序性

第12章 Java内存模型与线程

1. Java内存模型 Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的并发效果.在此之前,主流程序语言直接使用物理硬件(或者说是操作系统的内存模型),因此会由于不同平台上内存模型的差异,导致程序在一套平台上并发完全正常,而在另一套平台上并发访问却经常出错. 1. 主内存与工作内存 Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节.此处的

2015第27周三Java内存模型

自己写的代码,6个月不看也是别人的代码,自己学的知识也同样如此,学完的知识如果不使用或者不常常回顾,那么还不是自己的知识. 要认识java线程安全,必须了解两个主要的点:java的内存模型,java的线程同步机制.特别是内存模型,java的线程同步机制很大程度上都是基于内存模型而设定的. 浅谈java内存模型        不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的.其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的

【转】深入理解java内存模型

提纲 java线程之间的通信对程序员完全透明,内存可见性问题很容易困扰java程序员,本文试图揭开java内存模型神秘的面纱.本文大致分三部分:重排序与顺序一致性:三个同步原语(lock,volatile,final)的内存语义,重排序规则及在处理器中的实现:java内存模型的设计目标,及其与处理器内存模型和顺序一致性内存模型的关系. 深入理解java内存模型(一)——基础 深入理解java内存模型(二)——重排序 深入理解java内存模型(三)——顺序一致性 深入理解java内存模型(四)——

java内存模型(二)深入理解java内存模型的系列好文

深入理解java内存模型(一)--基础 深入理解java内存模型(二)--重排序 深入理解java内存模型(三)--顺序一致性 深入理解java内存模型(四)--volatile 深入理解java内存模型(五)--锁 深入理解java内存模型(六)--final 深入理解java内存模型(七)--总结 原文地址:https://www.cnblogs.com/jinggod/p/8495490.html

深入理解JVM读书笔记五: Java内存模型与Volatile关键字

12.2硬件的效率与一致性 由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了. 基于高速缓存的存储交互很好地理解了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题: 缓存一致性(Cache Coherenc

全面理解Java内存模型(JMM)及volatile关键字(转)

原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入理解Java并发之synchronized实现原理 Java并发编程-无锁CAS与Unsafe类及其并发包Atomic 深入理解Java内存模型(JMM)及volatile关键字 剖析基于并发AQS的重入锁(Reetr

Java内存模型与volatile关键字浅析

volatile关键字在java并发编程中是经常被用到的,大多数朋友知道它的作用:被volatile修饰的共享变量对各个线程可见,volatile保证变量在各线程中的一致性,因而变量在运算中是线程安全的.但是经过深入研究发现,大致方向是对的 ,但是细节上不是这样. 首先,引出volatile的作用.情景:当线程A遇到某个条件时,希望线程B做某件事.像这样的场景应该是经常会遇到的吧,下面我们来看一段模拟代码: package com.jack.jvmstudy; public class Test