Java内存模型分析

内存模型的概念:

在讲内存模型前,我们来谈谈硬件的效率与一致性的问题

计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。

由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,

而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,

会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,

那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,

再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:

[java] view plain copy

  1. int i = 1 ;  

[java] view plain copy

  1. i = i + 1;  

执行这句代码的线程首先会去物理主存中把 i 的值读取到,然后复制到CPU的高速缓存中,然后CPU在运算的时候就可以直接从高速缓存中

拿到值进行快速的计算,算完之后CPU再把结果丢到高速缓存中去,然后再刷新到主存中;

  这样的代码在单线程的情况下运行是完成没有问题的,但是在多线程的情况下会怎么样呢?

  可能会有这样的结果:第一个线程先从主存中读到了 i 的值,然后进行了+1的操作使 i = 2了, 此时它还没有将结果返回到主存中去,但是,第二个线程来  了,第二个线程读到的 i 的值是1 ,然后它在进行 + 1 ,结果为2 然后把结果返回给了主存, 在主存中,两个线程都问主存要了 i 的值,但是他们给的结果  都是2;按道理 i 应该是3 的; 这就是缓存不一致的问题了;

为了解决这个问题, 需要各个CPU处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作;出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

在多线程编程的环境下,我们常常遇到以下三个问题:原子性,可见性,有序性;

1、原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 关于原子性有一个很经典的例子就是银行账户转账问题: 比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。

如果原子性没有得到保证,A的账号上扣了钱,但是B的账户却没有加钱,这两个人是不是都去银行找麻烦去了?

举一个多线程的例子:给一个32位的变量进行赋值,如果原子性没有得到保证会发生什么情况?

[java] view plain copy

  1. i = 1 ;  

假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。

那么就可能发生一种情况:当将低16位数值写入之后,此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

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

    如果不能保证可见性,我们来看看会发生什么问题:

[java] view plain copy

  1. <span style="font-size:18px;">//线程一执行的代码
  2. int i = 1;  
  3. i = 100;  
  4. //线程二执行的代码
  5. int j = i;</span>  

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =100这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为100,那么在CPU1的高速缓存当中i的值变为100了,却没有立即写入到主存当中。此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是100.这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

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

我们来看下面这个例子:

[java] view plain copy

  1. <span style="font-size:18px;">int i = 0;  
  2. boolean  flag = false;  
  3. int i = 10; //语句1
  4. flag = true;  //语句2
  5. </span>  

这段代码先定义了两个变量,然后语句1和语句2 给这两个变量进行赋值;代码语句1在语句2前面,那语句1一定在语句2前面执行吗?答案是:不一定。

这里可能会发生指令冲排序;

      指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后       顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

显然单线程的情况下,不会出现问题,多线程的情况下,问题又来了;看例子:

[java] view plain copy

  1. //线程1
  2. resourse =  config.load(); //语句1:假设是加载一个文件完成初始化;
  3. boolean flag = true;//语句2 
  4. //线程2
  5. if(flag=false){  
  6.     Thread.sleep(1000);  
  7. }  

[java] view plain copy

  1. add();  

语句1跟语句2没有数据之间的依赖性,可能就会发生重排序;我们希望的是先加载完了再改变 变量 flag,结果可能是语句2先执行,语句1后执行,

这就可能发生文件还没加载完,后面的代码就执行了,逻辑没问题的话,发生异常是肯定的事。

从上面的解释来看:指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

2、Java内存模型;

在前面谈到了一些关于内存模型以及并发编程中可能会出现的一些问题。下面我们来看一下Java内存模型,

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

的内存访问效果,再次之前,主流程序语言(如C/C++)直接使用物理硬件和操作系统的内存模型,因此,会有雨不同平台上的内存模型的差异,有可能导致程序在一套平台上并发完成正常,而在另外一套平台上并发访问却经常出错,因此在某些场景就必须针对不同平台来编写程序;

      定义Java内存模型并非一件容易的事情,这个模型必须定义得足够严谨,才能让Java的并发内存访问操作不会产生歧义,但是,也必须定义得粗狗宽松,使得虚拟机的实现有足够的自由空间去利用硬件的各种特性来获取更好的执行速度,经过长时间的验证和修补,在JDK1.5发布后,Java内存模型已经成熟和完成起来了。

----摘自 深入理解Java虚拟机;

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

我们来看看Java的内存模型是怎样对 原子性、可见性以及有序性提供保证的?

1、原子性:

Java规定基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

提个问题,下面四个操作哪些是原子性的操作?

[java] view plain copy

  1. <span style="font-size:18px;">int a = 1;//语句1
  2. int j = a;//语句2
  3. i++;//语句3
  4. int b = a + 1;//语句4</span>

语句1:Java规定基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,:可得 语句1肯定是原子操作;

语句2:首先线程先得到a的值这是原子操作,然后再把a的值赋值给j,这也是原子操作,但是两个原子操作加起来就不是原子操作了;

语句3:先得到 i 的值,然后进行 ++ 操作 ,然后再将结果返回到主存,显然不是原子操作了;

语句4:同上,肯定不是原子操作;

可以看出,Java只对于基本数据类型的变量的读取和赋值操作是原子性操作,其他情况都不是原子操作了。要想保持其他行为是原子操作提供了两个方法,一个是使用synchronized关键字和使用Lock锁来实现;

2、可见性:

Java提供了volatile关键字、synchronized关键字和Lock锁来保证可见性。 volatile:当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。synchronized关键字和Lock锁:能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,因此也能保证可见性;

3、有序性:

在Java提供了volatile关键字、synchronized关键字和Lock锁来保证有序性。原理同上;

如果看完了有所收获,欢迎帮忙点下顶,告诉我我写的东西还是有用的。谢谢

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏PPV课数据科学社区

python多线程编程(1): python对多线程的支持

前面介绍过多线程的基本概念,理解了这些基本概念,掌握python多线程编程就比较容易了。 在开始之前,首先要了解一下python对多线程的支持。 虚拟机层面 P...

36715
来自专栏腾讯IVWEB团队的专栏

nodejs 中错误捕获的一些最佳实践

本文为翻译文章,原文比较长,感觉也有点啰嗦,所以根据个人理解猜测梳理出本文。

5950
来自专栏向治洪

dex分包方案

当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象: 1. 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILE...

2085
来自专栏芋道源码1024

【死磕Java并发】—–深入分析volatile的实现原理

通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized。如...

3505
来自专栏王亚昌的专栏

strace命令解析

strace常用于跟踪和分析进程执行时中系统调用和耗时以及占用cpu的比例,常用的格式如下:

1141
来自专栏吴生的专栏

谁说深入浅出虚拟机难?现在我让他通俗易懂(JVM)

1:什么是JVM 大家可以想想,JVM 是什么?JVM是用来干什么的?在这里我列出了三个概念,第一个是JVM,第二个是JDK,第三个是JRE。相信大家对这三个不...

3886
来自专栏我的博客

$_PUT?put数据获取

我们经常使用$_GET和$_POST来进行服务器交互,但是我们有的时候不得不被逼使用$_PUT方法获取数据 当然,php中是没有$_PUT的,但是我们可以使用...

5856
来自专栏Jerry的SAP技术分享

使用JavaScript给对象修改注册监听器

我们在开发一些大型前端项目时,会遇到这样一种情况,某个变量上有个字段。我们想知道是哪一段程序修改了这个变量上的字段。比如全局变量window上我们自定义了一个新...

822
来自专栏java一日一条

ava多线程:volatile变量、happens-before关系及内存一致性

请参考来自 Jean-philippe Bempel 的评论。他提到了一个真实因 JVM 优化导致死锁的例子。我尽可能多地写博客的原因之一是一旦自己理解错了,可...

762
来自专栏JAVA高级架构

《深入理解java虚拟机-高效并发》读书笔记

Java内存模型与线程 概述   多任务处理在现代计算机操作系统中几乎已是一项必备的功能,多任务运行是压榨手段,就如windows一样,我们使劲的压榨它运行多个...

3257

扫码关注云+社区

领取腾讯云代金券