原文地址:http://www.cnblogs.com/aigongsi/archive/2012/04/26/2470296.html;

近期在看《深入理解Java虚拟机:JVM高级特性与最佳实践》讲到了线程相关的细节知识,里面讲述了关于java内存模型,也就是jsr 133定义的规范。

系统的看了jsr 133规范的前面几个章节的内容,认为受益匪浅。废话不说,简要的介绍一下java内存规范。

什么是内存规范

在jsr-133中是这么定义的

A memory model describes, given a program and an execution trace of that program, whether

the execution trace is a legal execution of the program. For the Java programming language, the

memory model works by examining each read in an execution trace and checking that the write

observed by that read is valid according to certain rules.

也就是说一个内存模型描写叙述了一个给定的程序和和它的运行路径是否一个合法的运行路径。对于java序言来说,内存模型通过考察在程序运行路径中每个读操作,依据特定的规则,检查写操作相应的读操作能否是有效的。

java内存模型仅仅是定义了一个规范,详细的实现能够是依据实际情况自由实现的。可是实现要满足java内存模型定义的规范。

处理器和内存的交互

这个要感谢硅工业的发展,导致眼下处理器的性能越来越强大。眼下市场上基本上都是多核处理器。怎样利用多核处理器运行程序的优势,使得程序性能得到极大的提升,是眼下来说最重要的。

眼下全部的运算都是处理器来运行的,我们在大学的时候就学习过一个基本概念  程序 = 数据 + 算法 ,那么处理器负责计算,数据从哪里获取了?

数据能够存放在处理器寄存器里面(眼下x86处理都是基于寄存器架构的),处理器缓存里面,内存,磁盘,光驱等。处理器訪问这些数据的速度从快到慢依次为:寄存器,处理器缓存,内存,磁盘,光驱。为了加快程序运行速度,数据离处理器越近越好。可是寄存器,处理器缓存都是处理器私有数据,仅仅有内存,磁盘,光驱才是才是全部处理器都能够訪问的全局数据(磁盘和光驱我们这里不讨论,仅仅讨论内存)假设程序是多线程的,那么不同的线程可能分配到不同的处理器来运行,这些处理器须要把数据从主内存载入到处理器缓存和寄存器里面才干够运行(这个大学操作系统概念里面有介绍),数据运行完毕之后,在把运行结果同步到主内存。假设这些数据是全部线程共享的,那么就会发生同步问题。处理器须要解决何时同步主内存数据,以及处理运行结果何时同步到主内存,由于同一个处理器可能会先把数据放在处理器缓存里面,以便程序兴许继续对数据进行操作。所以对于内存数据,由于多处理器的情况,会变的非常复杂。以下是一个样例:

初始值 a = b = 0

process1           process2

1:load a               5:load b

2:write a:2           6:add b:1

3:load b               7: load a

4:write b:1           8:write a:1

如果处理器1先载入内存变量a,写入a的值为2,然后载入b,写入b的值为1,同一时候 处理2先载入b,运行b+1,那么b在处理器2的结果可能是1 可能是3。由于在load b之前,不知道处理器1是否已经吧b写会到主内存。对于a来说,如果处理器1后于处理器2把a写会到主内存,那么a的值则为2。

而内存模型就是规定了一个规则,处理器怎样同主内存同步数据的一个规则。

内存模型介绍

在介绍java内存模型之前,我们先看看两个内存模型

Sequential Consistency Memory Model:连续一致性模型。这个模型定义了程序运行的顺序和代码运行的顺序是一致的。也就是说 假设两个线程,一个线程T1对共享变量A进行写操作,另外一个线程T2对A进行读操作。假设线程T1在时间上先于T2运行,那么T2就能够看见T1改动之后的值。

这个内存模型比較简单,也比較直观,比較符合现实世界的逻辑。可是这个模型定义比較严格,在多处理器并发运行程序的时候,会严重的影响程序的性能。由于每次对共享变量的改动都要立马同步会主内存,不能把变量保存到处理器寄存器里面或者处理器缓存里面。导致频繁的读写内存影响性能。

Happens-Before Memory Model : 先行发生模型。

维基百科介绍:

In computer science, the happened-before relation (denoted: )
is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that. Even if those events are in reality executed out of order (usually to optimize program flow). This involves ordering events
based on the potential causal relationship of pairs of events
in a concurrent system, especially asynchronous distributed
systems
. It was formulated by Leslie Lamport.[1] In
Java specifically, a happens-before relationship is a guarantee that memory written to by statement A is visible to statement B, that is, that statement A completes its write before statement B starts its read.[1]

The processes that make up a distributed system have no knowledge of the happened-before relation unless they use a logical
clock
, like a Lamport clock or a vector
clock
.
This allows to design algorithms for mutual
exclusion
 and tasks like debugging or optimising distributed systems.

这个模型理解起来就比較困难。先介绍一个现行发生关系 (Happens-Before Relationship)

  假设有两个操作A和B存在A Happens-Before B,那么操作A对变量的改动对操作B来说是可见的。这个现行并非代码运行时间上的先后关系,而是保证运行结果是顺序的。看以下样例来说明现行发生

A,B为共享变量,r1,r2为局部变量
初始 A=B=0
Thread1 | Thread2
1: r2=A | 3: r1=B
2: B=2 | 4: A=2
  凭借直观感觉,线程1先运行 r2=A,则r2=0 ,然后赋值B=1,线程2运行r1=B,由于线程1改动了B的值为1,所以r1=1。可是在现行发生内存模型里面,有可能终于结果为r1 = r2 = 2。为什么会这样,由于编译器或者多处理器可能对指令进行乱序运行,线程1 从代码流上面看是先运行r2 = A,B = 1,可是处理器运行的时候会先运行 B = 2 ,在运行 r2 = A,线程2 可能先运行 A = 2 ,在运行r1 = B,这样可能 会导致 r1 = r2 = 2。

那我们先看看先行发生关系的规则

  • 1 在同一个线程里面,依照代码运行的顺序(也就是代码语义的顺序),前一个操作先于后面一个操作发生
  • 2 对一个monitor对象的解锁操作先于兴许对同一个monitor对象的锁操作
  • 3 对volatile字段的写操作先于后面的对此字段的读操作
  • 4 对线程的start操作(调用线程对象的start()方法)先于这个线程的其它不论什么操作
  • 5 一个线程中全部的操作先于其它不论什么线程在此线程上调用 join()方法
  • 6 假设A操作优先于B,B操作优先于C,那么A操作优先于C

解释一下以上几个先行发生规则的含义

规则1应该比較好理解,由于比較适合人正常的思维。比方在同一个线程t里面,代码的顺序例如以下:

thread1
共享变量A、B
局部变量r1、r2
代码顺序
1: A =1
2: r1 = A
3: B = 2
4: r2 = B
运行结果 就是 A=1 ,B=2 ,r1=1 ,r2=2

由于以上是在同一个线程里面,依照规则1 也就是依照代码顺序,A = 1 先行发生 r1 =A ,那么r1 = 1

再看规则2,以下是jsr133的样例

依照规则2,因为unlock操作先于发生于lock操作,所以X=1对线程2里面就是可见的,所以r2 = 1

在分析下面,看这个样例,因为unlock操作先于lock操作,所以线程x=1对于线程2不一定是可见(不一定是现行发生的),所以r2的值不一定是1,有可能是x赋值为1之前的那个状态值(如果x初始值为0,那么此时r2的值可能为0)

对于规则3,我们能够略微改动一下我们说明的第一个样例

A,B为共享变量,而且B是valotile类型的
r1,r2为局部变量
初始 A=B=0
Thread1 | Thread2
1: r2=A | 3: r1=B
2: B=2 | 4: A=2
那么r1 = 2, r2可能为0或者2

 由于对于volatile类型的变量B,线程1对B的更新立即线程2就是可见的,所以r1的值就是确定的。由于A是非valotile类型的,所以值不确定。

规则4,5,6这里就不解释了,知道规则就能够了。

能够从以上的看出,先行发生的规则有非常大的灵活性,编译器能够对指令进行又一次排序,以便满足处理器性能的须要。仅仅要又一次排序之后的结果,在单一线程里面运行结果是可见的(也就是在同一个线程里面满足先行发生原则1就能够了)。

java内存模型是建立在先行发生的内存模型之上的,而且再此基础上,增强了一些。由于现行发生是一个弱约束的内存模型,在多线程竞争訪问共享数据的时候,会导致不可预期的结果。有一些是java内存模型能够接受的,有一些是java内存模型不能够接受的。具体细节这里面就不具体说明了。这里仅仅说明关于java新的内存模型重要点。

final字段的语义

在java里面,假设一个类定义了一个final属性,那么这个属性在初始化之后就不能够在改变。一般觉得final字段是不变的。在java内存模型里面,对final有一个特殊的处理。假设一个类C定义了一个非static的final属性A,以及非static final属性B,在C的构造器里面对A,B进行初始化,假设一个线程T1创建了类C的一个对象co,同一时刻线程T2訪问co对象的A和B属性,假设t2获取到已经构造完毕的co对象,那么属性A的值是能够确定的,属性B的值可能还未初始化,

以下一段代码演示了这个情况

public class FinalVarClass {

    public final int a ;
public int b = 0; static FinalVarClass co; public FinalVarClass(){
a = 1;
b = 1;
} //线程1创建FinalVarClass对象 co
public static void create(){
if(co == null){
co = new FinalVarClass();
}
} //线程2訪问co对象的a,b属性
public static void vistor(){
if(co != null){
System.out.println(co.a);//这里返回的一定是1,a一定初始化完毕
System.out.println(co.b);//这里返回的可能是0,由于b还未初始化完毕
}
}
}

为什么会发生这样的情况,原因可能是处理器对创建对象的指令进行又一次排序。正常情况下,对象创建语句co = new FinalVarClass()并非原子的,简单来说,能够分为几个步骤,1 分配内存空间 2 创建空的对象 3 初始化空的对象 4 把初始化完毕的对象引用指向 co ,因为这几个步骤处理器可能并发运行,比方3,4 并发运行,所以在create操作完毕之后,co不一定立即初始化完毕,所以在vistor方法的时候,b的值可能还未初始化。可是假设是final字段,必须保证在相应返回引用之前初始化完毕。

volatile语义

对于volatile字段,在现行发生规则里面已经介绍过,对volatile变量的写操作先于对变量的读操作。也就是说不论什么对volatile变量的改动,都能够在其它线程里面反应出来。对于volatile变量的介绍能够參考 本人写的一篇文章 《java中volatilekeyword的含义
里面有具体的介绍。

volatile在java新的内存规范里面还加强了新的语义。在老的内存规范里面,volatile变量与非volatile变量的顺序是能够又一次排序的。举个样例

public class VolatileClass {

    int              x = 0;
volatile boolean v = false; //线程1write
public void writer() {
x = 42;
v = true;
}
//线程2 read
public void reader() {
if (v == true) {
System.out.println(x);//结果可能为0,可能为2
}
}
}

 线程1先调用writer方法,对x和v进行写操作,线程reader推断,假设v=true,则打印x。在老的内存规范里面,可能对v和x赋值顺序发生改变,导致v的写操作先行于x的写操作运行,同一时候另外一个线程推断v的结果,因为v的写操作先行于v的读操作,所以if(v==true)返回真,于是程序运行打印x,此时x不一定先行与System.out.println指令之前。所以显示的结果可能为0,不一定为2

可是java新的内存模型jsr133修正了这个问题,对于volatile语义的变量,自己主动进行lock 和 unlock操作包围对变量volatile的读写操作。那么以上语句的顺序能够表示为

thread1              thread2
1 :write x=1 5:lock(m) 2 :lock(m) 6:read v 3 :write v=true 7:unlock(m) 4 :unlock 8 :if(v==true) 9: System.out.print(x)

 因为unlock操作先于lock操作,所以x写操作5先于发生x的读操作9

以上仅仅是jsr规范中一些小结行的内容,因为jsr133规范定义了非常多术语以及非常多推论,上述仅仅是简单的介绍了一些比較重要的内容,详细细节能够參考jsr规范的public view :http://today.java.net/pub/a/today/2004/04/13/JSR133.html

Java内存模型-jsr133规范介绍的更多相关文章

  1. Java内存模型-jsr133规范介绍(转)

    最近在看<深入理解Java虚拟机:JVM高级特性与最佳实践>讲到了线程相关的细节知识,里面讲述了关于java内存模型,也就是jsr 133定义的规范. 系统的看了jsr 133规范的前面几 ...

  2. Java内存模型概念简单介绍,想深入自行百度

  3. 并发编程-Java内存模型

    将之前看过的关于并发编程的东西总结记录一下,本文简单记录Java内存模型的相关知识. 1. 并发编程两个关键问题 并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步. (1)在命令式 ...

  4. 面试官:为什么需要Java内存模型?

    面试官:今天想跟你聊聊Java内存模型,这块你了解过吗? 候选者:嗯,我简单说下我的理解吧.那我就从为什么要有Java内存模型开始讲起吧 面试官:开始你的表演吧. 候选者:那我先说下背景吧 候选者:1 ...

  5. 深入浅出Java内存模型

    面试官:我记得上一次已经问过了为什么要有Java内存模型 面试官:我记得你的最终答案是:Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,保证了Java程序在各种平台 ...

  6. 深入理解 Java 内存模型(一)- 内存模型介绍

    深入理解 Java 内存模型(一)- 内存模型介绍 深入理解 Java 内存模型(二)- happens-before 规则 深入理解 Java 内存模型(三)- volatile 语义 深入理解 J ...

  7. JSR133提案-修复Java内存模型

    目录 1. 什么是内存模型? 2. JSR 133是关于什么的? 3. 再谈指令重排序 4.同步都做了什么? 5. final字段在旧的内存模型中为什么可以改变? 6."初始化安全" ...

  8. 从原子类和Unsafe来理解Java内存模型,AtomicInteger的incrementAndGet方法源码介绍,valueOffset偏移量的理解

    众所周知,i++分为三步: 1. 读取i的值 2. 计算i+1 3. 将计算出i+1赋给i 可以使用锁来保持操作的原子性和变量可见性,用volatile保持值的可见性和操作顺序性: 从一个小例子引发的 ...

  9. 【Todo】【转载】深入理解Java内存模型

    提纲挈领地说一下Java内存模型: 什么是Java内存模型 Java内存模型定义了一种多线程访问Java内存的规范.Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几 ...

随机推荐

  1. Android 内存管理之优化建议

    OOM(OutOfMemory)转:http://hukai.me/android-performance-oom/ 前面我们提到过使用getMemoryClass()的方法可以得到Dalvik He ...

  2. APP分享抓取网页图片

    var getShareImages = { defaultimg:"defaultimg.png", _allImgs:null, init:function(){ getSha ...

  3. 自学HTML5第二节(标签篇---新增标签详解)

    HTML5新增标签: <article> 标签 规定独立的自包含内容.一篇文章应有其自身的意义,应该有可能独立于站点的其余部分对其进行分发. <article> 元素的潜在来源 ...

  4. 怎样在Eclipse中使用debug模式调试程序

    最基本的操作是: 1, 首先在一个java文件中设断点,然后运行,当程序走到断点处就会转到debug视图下, 2, F5键与F6键均为单步调试,F5是step into,也就是进入本行代码中执行,F6 ...

  5. 最近招两个兼职的活(PHP和JSP)

    我这里的活,都是兼职写作的,是两本入门教程, 一本是PHP+Nginx 一本是JSP+Servlet. 都是入门教程,有署名有稿酬,有兴趣的可以联系 QQ:837652732  验证:PHP或Java ...

  6. vim-配置文件

    " " Last Change: 2010年08月02日 15时13分 " " Version: 1.80 " "============= ...

  7. 解决cookie无法删除的问题

    今天遇到一个cookie无法删除的问题,退出操作时cookie无法删除,必须在首页先进行退出操作,后来发现一个网友的博客,介绍了无法删除Cookie的原因,原来是我关于cookie的基础知识没搞清楚. ...

  8. mysql字符集编码乱码测试如下

    创建三个表tb_latin1,tb_utf8,tb_gbk,编码分别为latin1/utf8/gbk “你好a”字符串编码如下GBK : %C4%E3 %BA%C3 %61UTF-8 : %E4%BD ...

  9. Android文件存储使用参考

    可能遇到的问题 android系统自身自带有存储,另外也可以通过sd卡来扩充存储空间.前者好比pc中的硬盘,后者好移动硬盘. 前者空间较小,后者空间大,但后者不一定可用. 开发应用,处理本地数据存取时 ...

  10. JavaScript之共享onload

    我们知道,当我们将JS代码脚本放到<head></head>标签之间时,这是的js代码加载要先于DOM加载,而我们往往会在JS代码脚本中写一些获取DOM元素的代码,而此时的DO ...