Java并发编程:JMM(Java内存模型)和volatile

上传人:飞*** 文档编号:44523395 上传时间:2018-06-09 格式:DOCX 页数:12 大小:400.81KB
返回 下载 相关 举报
Java并发编程:JMM(Java内存模型)和volatile_第1页
第1页 / 共12页
Java并发编程:JMM(Java内存模型)和volatile_第2页
第2页 / 共12页
Java并发编程:JMM(Java内存模型)和volatile_第3页
第3页 / 共12页
Java并发编程:JMM(Java内存模型)和volatile_第4页
第4页 / 共12页
Java并发编程:JMM(Java内存模型)和volatile_第5页
第5页 / 共12页
点击查看更多>>
资源描述

《Java并发编程:JMM(Java内存模型)和volatile》由会员分享,可在线阅读,更多相关《Java并发编程:JMM(Java内存模型)和volatile(12页珍藏版)》请在金锄头文库上搜索。

1、Java 并发编程:JMM(Java 内存模型)和 volatile1. 并发编程的并发编程的 3 个概念个概念并发编程时,要想并发程序正确地执行,必须要保证原子性、可见性和有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。1.1. 原子性原子性原子性:即一个或多个操作要么全部执行并且执行过程中不会被打断,要么都不执行。一个经典的例子就是银行转账:从账户 A 向账户 B 转账 1000 元,此时包含两个操作:账户 A 减去 1000 元,账户 B 加上 1000 元。这两个操作必须具备原子性才能保证转账安全。假如账户 A 减去 1000 元之后,操作被打断了,账户 B 却没有收到转过

2、来的 1000 元,此时就出问题了。 1.2. 可见性可见性可见性:即多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的最新值。例如下段代码,线程 1 修改 i 的值,线程 2 却没有立即看到线程 1 修改的 i 的最新值:/线程 1 执行的代码intint i = 0 0;i = 1010;/线程 2 执行的代码j = i;假如执行线程 1 的是 CPU1,执行线程 2 的是 CPU2。当线程 1 执行 i=10 时,会将CPU1 的高速缓存中 i 的值赋值为 10,却没有立即写入主内存中。此时线程 2 执行 j=i,会先从主内存中读取 i 的值并加载到 CP

3、U2 的高速缓存中,此时主内存中的 i=0,那么就会使得 j 最终赋值为 0,而不是 10。1.3. 有序性有序性有序性:即程序执行的顺序按代码的先后顺序执行。例如下面这段代码:intint i = 0 0;booleanboolean flag = falsefalse;i = 1 1;flag = truetrue;在代码顺序上 i=1 在 flag=true 前面,而 JVM 在真正执行代码的时候不一定能保证 i=1 在 flag=true 前面执行,这里就发生了指令重排序。指令重排序指令重排序一般是为了提升程序运行效率,编译器或处理器通常会做指令重排序:编译器重排序:编译器在不改变单线

4、程程序语义的前提下,可以重新安排语句的执行顺序处理器重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。CPU 在指令重排序时会考虑指令之间的数据依赖性,如果指令 2 必须依赖用到指令 1 的结果,那么 CPU 会保证指令 1 在指令 2 之前执行。指令重排序不保证程序中各个语句的执行顺序和代码中的一致,但会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上例中的代码, i=1 和 flag=true 两个语句先后执行对最终的程序结果没有影响,就有可能 CPU 先执行 flag=true,后执行 i=1。2. java 内存模型内存模型由于 volatile 关键字是

5、与 java 内存模型相关的,因此了解 volatile 前,需要先了解下 java 内存模型相关概念 2.1. 硬件效率与缓存一致性硬件效率与缓存一致性计算机执行程序时,每条指令都是在 CPU 中执行的,而执行指令过程中,势必涉及到数据的读取和写入。CPU 在与内存交互时,需要读取运算数据、存储结果数据,这些 I/O 操作的速度与 CPU 的处理速度有几个数量级的差距,所以不得不加入一层读写速度尽可能接近 CPU 运算速度的高速缓存(Cache)来作为内存与 CPU 之间的缓冲:将运算需要使用的数据复制到高速 Cache 中;运算结束后再从高速 Cache 同步回内存中。这样 CPU 就无需

6、等待缓慢的内存读写了。这在单线程中运行是没有问题的,但在多线程中运行就引入了 缓存一致性 的问题:在多处理系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个 CPU 的运算任务都涉及同一主内存区域时,将可能导致各自的缓存数据不一致,此时同步回主内存时以谁的数据为准呢?为了解决缓存一致性问题,通常有两种解决方法:在总线加 LOCK# 锁的方式缓存一致性协议早期的 CPU 中,通过在总线上加 LOCK# 锁的形式来解决,因为 CPU 在和其他部件通信时都是通过总线进行,如果对总线加 LOCK# 锁,也就阻塞了 CPU 对其他部件访问(如内存),而使得只能有一个 CPU 使用这个变

7、量的内存。但这种方式有一个问题,在锁住总线期间,其他 CPU 无法访问内存,导致效率低下。所有就出现了缓存一致性协议,最著名的就是 Intel 的 MESI 协议,MESI 协议保证了每个缓存中使用的共享变量的副本是一致的, 它的核心思想是:CPU 写数据时,如果操作的变量是共享变量(其他 CPU 的高速缓存中也存在该变量的副本),会发出信号通知其他 CPU 将该变量的缓存设置为无效状态,那么当其他 CPU 读取该变量时,就会从内存重新读取。JVM 有自己的内存模型,在访问缓存时,遵循一些协议来解决缓存一致性的问题。2.2. 主内存和工作内存主内存和工作内存Java 虚拟机规范中试图定义一种

8、Java 内存模型(JMM, Java Memory Model)来屏蔽硬件和操作系统的内存访问差异,实现 Java 程序在各种平台上达到一致的内存访问效果。Java 内存模型主要目标:是定义程序中各个变量的访问规则,即存储变量到内存和从内存中取出变量这样的底层细节。为了较好的执行性能,Java 内存模型并没有限制使用 CPU 的寄存器和高速缓存来提升指令执行速度,也没有限制编译器对指令做重排序。也就是说:在 Java 内存模型中,也会存在缓存一致性问题和指令重排序问题。Java 内存模型规定所有的变量(包括实例字段、静态字段、构成数组对象的元素,不包括线程私有的局部变量和方法参数,因为这些不

9、会出现竞争问题)都存储在主内存中,每条线程有自己的工作内存(可与之前将讲的 CPU 高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存拷贝副本。线程对变量的所有操作(read,write)都必须在工作内存中进行,而不能直接读写主内存中的变量,线程间变量值的传递需要通过主内存来完成。如图所示: 2.3. JMM 如何处理原子性如何处理原子性像以下语句:x = 1010; /语句 1y = x; /语句 2x+; /语句 3x = x + 1 1; /语句 4只有语句 1 才是原子性的操作,其他都不是原子性操作。 语句 1 是直接将 10 赋值给 x 变量,也就是说线程执行这个语句

10、时,会直接将 10 写入到工作内存中。 语句 2 包含了两个操作,先读取 x 的值,然后将 x 的值写入到工作内存赋值给 y,这两个操作合起来就不是原子性操作了。 语句 3 和 4 都包括 3 个操作,先读取 x 的值,然后加 1 操作,最后写入新值。单线程环境下,我们可以认为整个步骤都是原子性的。但多线程环境下则不同,只有基本数据类型的访问读写是具备原子性的,如果还需要提供更大范围的原子性保证,可以使用同步代码块 - synchronized 关键字。在 synchronized 块之间的操作具备原子性。2.4. JMM 如何处理可见性如何处理可见性Java 内存模型是通过变量修改后将新值同

11、步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性的。普通变量和 volatile 变量都如此,区别在于:volatile 特殊规则保证了新值能立即同步回主内存,以及每次改前立即从主内存刷新。因此 volatile 变量保证了多线程操作时变量的可见性而普通变量无法保证这一点,因为普通的共享变量修改后,什么时候同步写回主内存是不确定的,其他线程读取时,此时内存中的可能还是原来的旧值。除了 volatile 变量外,synchronized 和 final 关键字也能实现可见性。synchronized 同步块的可见性是由:对一个变量执行 unlock 操作前,必

12、须先把此变量同步回主内存中 这条规则获得的。final 可见性是指:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 this 的引用传递出去,那在其他线程中就能看见 final 字段的值。2.5. JMM 如何处理有序性如何处理有序性Java 程序中天然的有序性可概括为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句指:线程内表现为串行语义,后半句是指:指令重排序现象和工作内存和主内存同步延迟现象Java 中提供了 volatile 和 synchronized 关键字来保证线程之间操作的有序性。volatil

13、e 本身就包含了禁止指令重排序的语义,而 synchronized 是由一个变量在同一时刻只允许一条线程对其 lock 操作这条规则获得,这条规则决定了持有同一个锁的两个同步代码块只能串行的执行。happens-before 原则原则Java 内存模型中,有序性保证不仅只有 synchronized 和 volatile,否则一切操作都将变得繁琐。Java 中还有一个 happens-before 原则,它是判断线程是否安全的主要依据。依靠这个规则,可以保证程序的有序性,如果两个操作的执行顺序无法从 happens-before 原则中推导出来,则他们就不能保证有序性,可以随意重排序。happ

14、ens-before(先行发生先行发生) 是 Java 内存模型中定义的两项操作之间的偏序关系,如果操作 A 先行发生于操作 B,那么就是说发生操作 B 之前,操作 A 产生的影响能被操作B 观察到。影响包括修改内存中共享变量的值、发送了消息、调用了方法等。下面是 Java 内存模型下的天然的先行发生关系,这些关系无需任何同步就已经存在:程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确的来说,应该是控制流顺序,而不是代码顺序,因为要考虑分支、循环等结构管程锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作volatile 变量规

15、则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作线程启动规则:Thread 对象的 start() 方法先行发生于此线程的每个一个动作线程终止规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程是否已经终止执行线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可通过 Thread.isinterrupted() 检测是否有中断发生对象终结规则:一个对象的初始化完成先行发生于他的 finalize() 方法的开始

16、传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C第一条程序次序原则,“书写在前面的操作先行发生于书写在后面的操作“,这个应该是一段程序代码的执行在单线程中看起来是有序的,因为虚拟机可能对程序代码中不存在数据依赖性的指令进行重排序,但最终执行结果与顺序执行的结果是一致的。而在多线程中,无法保证程序执行的有序性。第二条,第三条分别是关于 synchronized 同步块 和 volatile 的规则。第四至第七条是关于 Thread 线程的规则。第八条是体现了 happens-before 原则的传递性。下面是一个利用 happens-before 规则判断操作间是否具备顺序性的例子:privateprivate intint value=0 0;publicpublic voidvoid setValuesetValue() thisthis.value = value;publicpublic intint setValuesetValu

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 行业资料 > 其它行业文档

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号