JVM学习(3)——总结Java内存模型---转载自http://www.cnblogs.com/kubixuesheng/p/5202556.html
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下:
- 为什么学习Java的内存模式
- 缓存一致性问题
- 什么是内存模型
- JMM(Java Memory Model)简介
- volatitle关键字
- 原子性
- 可见性
- 有序性
- 指令重排
- 先行发生——happen-before原则
- 解释执行和编译执行
- 其他语言(c和c++)也有内存模型么?
为什么需要关注Java内存模型?
众所周知,计算机某个运算的完成不仅仅依靠cpu及其寄存器,还要和内存交互!cpu需要读取内存中的运行数据,存储运算结果到内存中……其中很自然的也是无法避免的就涉及到了I/O操作,而常识告诉我们,I/O操作和cpu的运算速度比起来,简直没得比!前者远远慢于后者(书上说相差几个数量级!),前面JVM学习2也总结了这个情景,人们解决的方案是加缓存——cache(高速缓存),cache的读写速度尽可能的接近cpu运算速度,来作为内存和cpu之间的缓冲!旧的问题解决了,但是引发了新的问题!如果有多个cpu怎么办?
现代操作系统都是多核心了,如果多个cpu和一块内存进行交互,那么每个cpu都有自己的高速缓存块……咋办?也就是说,多个cpu的运算都访问了同一块内存块的话,可能导致各个cpu的缓存数据不一致!if发生了上述情景,then以哪个cpu的缓存为主呢?为了解决这个问题,人们想到,让各个cpu在访问缓存时都遵循某事先些规定的协议!因为无规矩不成方圆!如图(现在可以回答什么是内存模型了):
什么是内存模型?
通俗的说,就是在某些事先规定的访问协议约束下,计算机处理器对内存或者高速缓存的访问过程的一种抽象!这是物理机下的东西,其实对虚拟机来说(JVM),道理是一样的!
什么是Java的内存模型(JMM)?
教科书这样写的:JVM规范说,Java程序在各个os平台下必须实现一次编译,到处运行的效果!故JVM规范定义了一个模型来屏蔽掉各类硬件和os之间内存访问的差异(比如Java的并发程序必须在不同的os下运行效果是一致的)!这个模型就是Java的内存模型!简称JMM。
让我通俗的说:Java内存模型定义了把JVM中的变量存储到内存和从内存中读取出变量的访问规则,这里的变量不算Java栈内的局部变量,因为Java栈是线程私有的,不存在共享问题。细节上讲,JVM中有一块主内存(不是完全对应物理机主内存的那个概念,这里说的JVM的主内存是JVM的一部分,它主要对应Java堆中的对象实例及其相关信息的存储部分)存储了Java的所有变量。且Java的每一个线程都有一个工作内存(对应Java栈),里面存放了JVM主内存中变量的值的拷贝!且Java线程的工作内存和JVM的主内存独立!如图:
当数据从JVM的主内存复制一份拷贝到Java线程的工作内存存储时,必须出现两个动作:
- 由JVM主内存执行的读(read)操作
- 由Java线程的工作内存执行相应的load操作
反过来,当数据从线程的工作内存拷贝到JVM的主内存时,也出现两个操作:
- 由Java线程的工作内存执行的存储(store)操作;
- 由JVM主内存执行的相应的写(write)操作
read,load,store,write的操作都是原子的,即执行期间不会被中断!但是各个原子操作之间可能会发生中断!对于普通变量,如果一个线程中那份JVM主内存变量值的拷贝更新了,并不能马上反应在其他变量中,因为Java的每个线程都私有一个工作内存,里面存储了该条线程需要用到的JVM主内存中的变量拷贝!(比如实例的字段信息,类型的静态变量,数组,对象……)如图:
A,B两条线程直接读or写的都是线程的工作内存!而A、B使用的数据从各自的工作内存传递到同一块JVM主内存的这个过程是有时差的,或者说是有隔离的!通俗的说他们之间看不见!也就是之前说的一个线程中的变量被修改了,是无法立即让其他线程看见的!如果需要在其他线程中立即可见,需要使用 volatile 关键字。现在引出volatile关键字:
volatile 关键字是干嘛的?举例说明。
前面说了,各个线程之间的变量更新,如果想让其他线程立即可见,那么需要使用它,故volatile字段是用于线程间通讯的特殊字段。每次读volatile字段都会看到其它线程写入该字段的最新值!也就是说,一旦一个共享变量(成员、静态)被volatile修饰,那么就意味着:a线程修改了该变量的值,则这个新的值对其他线程来说,是立即可见的!先看一个例子:
这段代码会完全运行正确么?即一定会中断么?
//线程A
boolean stop = false; while(!stop){
doSomething();
} //=========
//线程B
stop = true;
有些人在写程序时,如果需要中断线程,可能都会采用这种办法。但是这样做是有bug的!虽然这个可能性很小,但是只要一旦bug发生,后果很严重!前面已经说了,Java的每个线程在运行过程中都有自己的工作内存,且Java的并发模型采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明,这也是为什么如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,则很可能会遇到各种奇怪的并发问题的原因。针对本题的A、B线程,如果他们之间通信,画成图是这样的:
那么线程A和B需要通信的时候,第一步A线程会将本地工作内存中的stop变量的值刷新到JVM主内存中,主内存的stop变量=false,第二步,线程B再去主内存中读取stop的拷贝,临时存储在B,此时B中工作内存的stop也为false了。当线程B更改了stop变量的值为true之后,同样也需要做类似线程A那样的工作……但是此时此刻,恰恰B还没来得及把更新之后的stop写入主存当中(前面说了各个原子操作之间可以中断),就转去做其他事情了,那么线程A由于不知道线程B对stop变量的更改,因此还会一直循环下去。这就是死循环的潜在bug!
如果stop使用了volatile修饰,会使得:
- B线程更新stop值为true,会强制将修改后的值立即写入JVM主内存,不许原子操作之间中断。
- 线程B修改stop时,也会让线程A的工作内存中的stop缓存行失效!因为A线程的工作内存中JVM主内存的stop的拷贝值缓存行无效了,所以A线程再次读取stop的值会去JVM主内存读取
这样A得到的就是最新的正确的stop值——true。程序完美的实现了中断。很多人还认为,volatile这么好,它比锁的性能好多了!其实这不是绝对的,很片面,只能说volatile比重量级的锁(Java中线程是映射到操作系统的原生线程上的,如果要唤醒或者是阻塞一条线程需要操作系统的帮忙,这就需要从用户态转换到核心态,而状态转换需要相当长的时间……所以说syncronized关键字是java中比较重量级的操作)性能好,而且valatile万万不能代替锁,因为它不是线程安全的,既volatile修饰符无法保证对变量的任何操作都是原子的!(鉴于主要涉及了Java的并发编程,之后再开专题总结)。
什么是原子性?
在Java中,对基本数据类型的变量的操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。看例子:
1 int x = 10; //语句1
2 y = x; //语句2
3 x++; //语句3
4 x = x + 1; //语句4
这几个语句哪个是原子操作?
其实只有语句1是原子性操作,其他三个语句都不是原子性操作。语句1是直接将数值10赋值给x,也就是说线程执行这个语句会直接将数值10写入到工作内存中。线程执行语句2实际上包含2个操作,它先要去主内存读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存这2个操作都是原子性操作,但是合起来就不是原子性操作了。同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。所以上面4个语句只有语句1的操作具备原子性。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
何时使用volatile关键字?
通常来说,使用volatile必须具备以下2个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。比如boolean类型的标记变量。
前面只是大概总结了下Java的内存模式和volatile关键字,不是很深入,留待后续并发专题补充。下面接着看几个之前和之后会遇到的概念:
到底什么是可见性?如何保证?
- 指令重排(破坏线程间的有序性)
- 之前说的工作内存和主内存同步延时(也就是线程A先后更新两个变量m和n,但是由于线程工作内存和JVM主内存之间的同步延时,线程B可能还没完全同步线程A更新的两个变量,可能先看到了n……对于B来说,它看A的操作就是无序的,顺序无法保证)。
谈谈对指令重排的理解
a=1;
b=2;
先给a赋值,和先给b赋值,其实没什么区别,效果是一样的,这样的代码就是可重排代码,编译器会针对上下文对指令做顺序调整,哪个顺序好,就用哪个,所以实际上两句话怎么个执行顺序,是不一定的。
有可重排就自然会有不可重排,首先要知道Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够保证有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。反之遵循了happen-before原则,JVM就无法对指令进行重排序(看起来的)。这样又引出了一个新问题:
什么是先行发生原则happens-before?
下面就来具体介绍下happens-before(先行发生原则,这里的先行和时间上先行是两码事;):
- 程序次序规则:在一个线程内,书写在前面的操作先行发生于书写在后面的操作,就像刚刚说的,一段代码的执行在单个线程中看起来是有序的,程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作,这是一条比较重要的规则。就是说如果一个线程先去写一个volatile变量,然后另一个线程去读取,那么写入操作肯定会先行发生于读操作。
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C,实际上就是体现happens-before原则具备传递性。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,Thread.join()。
- 对象终结规则:一个对象的初始化完成(构造器执行结束)先行发生于他的finalize()方法的开始
前4条规则是比较重要的,后4条规则都是常识。
比如像如下这样的线程内的串行语义()是不可重排语句:
- 写后读
a = 1;
b = a;// 写一个变量之后,再读这个变量。
- 写后写
a = 1;
a = 2; // 写一个变量之后,再写这个变量。
- 读后写
a = b;
b = 1; // 读一个变量之后,再写这个变量。
以上语句不可重排,单线程的程序看起来执行的顺序是按照代码顺序执行的,这句话要正确理解:JVM实际上还是可能会对程序代码不存在数据依赖性的指令进行指令重排序,虽然进行重排序,但是最终执行的结果是与单线程的程序顺序执行的结果一致的。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。对于多线程环境,编译器不考虑多线程间的语义。看一个例子:
1 class OrderExample {
2 private int a = 0;
3
4 private boolean flag = false;
5
6 public void writer() {
7 a = 1;
8 flag = true;
9 }
10
11 public void reader() {
12 if (flag) {
13 int i = a + 1;
14 }
15 }
16 }
让线程A首先执行writer()方法,接着让线程B线程执行reader()方法,线程B如果看到了flag,那么就可能会立即进入if语句,但是在int i=a+1处不一定能看到a已经被赋值为1,因为在writer中,两句话顺序可能打乱!有可能对于B线程,它看A是无序的!编译器无法保证有序性。因为A完全可以先执行flag=true,再执行a=1,不影响结果!如图:
也就是说多线程之间无法保证指令的有序性!先行发生原则的程序次序有序性原则是针对单线程的。也就是说,如果是一个线程去先后执行这两个方法,完全是ok的!符合happens-before原则的第一条——程序次序有序性,故不存在指令重排问题。
如何解决呢?还是套用先行发生原则,看第二条锁定原则,我们可以使用同步锁:
class OrderExample {
private int a = 0; private boolean flag = false; public synchronized void writer() {
a = 1;
flag = true;
} public synchronized void reader() {
if (flag) {
int i = a + 1;
}
}
}
因为写、读都加锁了,他们之间本质是串行的,即使线程A占有写锁期间,JVM对写做了指令重排也没关系,因为此时锁被A拿了,B线程无法执行读操作,直到A线程把写操作执行完毕,释放了该锁,B线程才能拿到这同一个对象锁,而此时,a肯定是1,flag也必然是true了。此时必然是有序的。通俗的说,同步后,即使做了重排,因为互斥的缘故,reader 线程看writer线程也是顺序执行的。
其他语言(c和c++)也有内存模型么?
大部分其他的语言,像C和C++,都没有被设计成直接支持多线程。这些语言对于发生在编译器和处理器平台架构的重排序行为的保护机制会严重的依赖于程序中所使用的线程库(例如pthreads),编译器,以及代码所运行的平台所提供的保障。
最后补充下一个问题:Java的字节码两种运行方式——解释执行和编译执行
- 解释运行:解释执行以解释方式运行字节码,解释执行的意思是:读一句执行一句。
- 编译运行(JIT):将字节码编译成机器码,直接执行机器码,是在运行时编译(不是代码写完了编译的),编译后性能有数量级的提升(能差10倍以上)
JVM学习(3)——总结Java内存模型---转载自http://www.cnblogs.com/kubixuesheng/p/5202556.html的更多相关文章
- Java内存模型(转载)
1. 概述 多任务和高并发是衡量一台计算机处理器的能力重要指标之一.一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS)这个指标比较能说明问题 ...
- 深入理解Java内存模型--转载
原文地址:http://www.infoq.com/cn/articles/java-memory-model-1 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之 ...
- java并发学习--第十章 java内存模型的内存语义
一.锁的内存语义 所为的java内存模型的内存语义指的就是在JVM中的实现原则. 锁的内存语义:锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息. 我们把上面这句话再整理下: ...
- 从JVM设计角度解读Java内存模型
第十六章:Java内存模型 本文我们将重点放在Java内存模型(JMM)的一些高层设计问题,以及JMM的底层需求和所提供的保证,还有一些高层设计原则背后的原理. 例如安全发布,同步策略的规范以及一致性 ...
- JVM(一)Java内存模型
前言 对于从事C.C++程序开发的开发人员来说,在开始使用对象之前,他们都需要使用new关键字为对象申请内存空间,在使用完对象之后,也需要使用delete关键字来释放对象占用的内存空间.对于Java程 ...
- Java并发(1)- 聊聊Java内存模型
引言 在计算机系统的发展过程中,由于CPU的运算速度和计算机存储速度之间巨大的差距.为了解决CPU的运算速度和计算机存储速度之间巨大的差距,设计人员在CPU和计算机存储之间加入了高速缓存来做为他们之间 ...
- JVM学习(3)——总结Java内存模型
俗话说,自己写的代码,6个月后也是别人的代码……复习!复习!复习!涉及到的知识点总结如下: 为什么学习Java的内存模式 缓存一致性问题 什么是内存模型 JMM(Java Memory Model)简 ...
- JVM 学习(二)Java 内存模型、方法内联、逃逸 --- 2019年4月
1.Java 的内存模型 定义了 happens-before,如果同一个线程中,字节码的先后顺序,后者观测了前者的运行结果,那么就会按顺序执行. Java 线程之间的通信由 Java 内存模型控制. ...
- JVM学习记录-Java内存模型(一)
前言 Java虚拟机规范中定义了一种Java的内存模型,即Java Memoory Model(简称JMM),用来实现让Java程序在各个平台下都能达到一致的内存访问效果.JVM是整个虚拟机,JMM模 ...
随机推荐
- Python3正则表示式(3)
正则表示式对象 对象1: 案例1: import re example = 'ShanDong Institute of Business and Technology' pattern = re.c ...
- config、option、setting辨析
作为一个编程新手,在软件目录中常常会看到这几个词,尤其 config . setting 翻译成中文区别不是很大,总让人有点区分不了他们的使用场景.在知乎上看到了关于这个问题的讨论觉得受益挺大的,自己 ...
- php中对Mysql数据库的访问操作
一: PHP-MySQL 是 PHP 操作 MySQL 资料库最原始的 Extension ,PHP-MySQLi 的 i 代表 Improvement ,提更了相对进阶的功能,就 Extensio ...
- API使用
至于什么是API我想不用累述了,百科上面有,其实就是别人写好了一大堆功能性的代码,然后你可以拿来用.一般的二次开发都是使用api来开发,包括现在的高级程序设计,很少自己写基本代码了,像.netFram ...
- 实例化和设置一个优秀的php对象
类是用于生成对象的代码模板,对象可以被说成是类的"实例" class ShopProduct{ public $title = 'default product'; // 属性也称 ...
- C# 调用windows api 操作鼠标、键盘、窗体合集...更新中
鼠标操作window窗体合集...更新中 1.根据句柄查找窗体 引自http://www.2cto.com/kf/201410/343342.html 使用SPY++工具获取窗体 首先打开spy+ ...
- [原创]zabbix工具介绍,安装及使用
[原创]zabbix工具介绍,安装及使用 http://waringid.blog.51cto.com/65148/955939/
- .NetCore中EFCore的使用整理
EntirtyFramework框架是一个轻量级的可扩展版本的流行实体框架数据访问技术. 其中的.NetCore版本对应EntityFrameworkCore Git源代码地址:https://git ...
- windows server 2008 R2安装图片浏览器/照片查看器方法
有用户的电脑安装了windows server 2008 R2,浏览大量图片时很不方便,因为系统中没有照片查看器或图片浏览器.其实,win2008 R2是有照片查看器的,只是默认情况下没有开启.参考以 ...
- [Python设计模式] 第7章 找人帮忙追美眉——代理模式
github地址:https://github.com/cheesezh/python_design_patterns 题目1 Boy追求Girl,给Girl送鲜花,送巧克力,送洋娃娃. class ...