你真的了解JMM吗?
引言
在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,举例说明变量在多个CPU之间的共享。如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。
一、JMM(Java Memory Model)
java虚拟机规范定义java内存模型屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
java内存模型规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
注意:我们这里强调的是共享变量,不是私有变量。
java内存模型规定了所有的变量都存储在主内存中(JVM内存的一部分)。每条线程都有自己的工作内存,工作内存中保存了该线程使用的主内存中共享变量的副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量;工作内存在线程间是隔离的,不能直接访问对方工作内存中的变量。所以在多线程操作共享变量时,就通过JMM来进行控制。
我们来看一看线程,工作内存、主内存三者的交互关系图。
二、JMM的8种内存交互操作
9龙就疑问,JMM是如何保证并发下数据的一致性呢?
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态。
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load (载入):作用于工作内存的变量,它把read操作从主存中得到变量放入工作内存的变量副本中。
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store (存储):作用于工作内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
如果是将变量从主内存复制到工作内存,必须先执行read,后执行load操作;如果是将变量从工作内存同步到主内存,必须先执行store,后执行write。JMM要求read和load, store和write必须按顺序执行,但不是必须连续执行,中间可以插入其他的操作。
2.1、JMM指令使用规则
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
三、volatile
很多并发编程中都使用了volatile,你知道为什么一个变量要使用volatile修饰吗?
volatile有两个语义:
- volatile可以保证线程间变量的可见性。
- volatile禁止CPU进行指令重排序。
volatile修饰的变量,如果某个线程更改了变量值,其他线程可以立即观察到这个值。而普通变量不能做到这一点,变量值在线程间传递均需要主内存来完成。如果线程修改了普通变量值,则需要刷新回主内存,另一个线程需要从主内存重新读取才能知道最新值。
3.1、volatile只能保证可见性,不能保证原子性
虽然volatile只能保证可见性,但不能认为volatile修饰的变量可以在并发下是线程安全的。
public class VolatileTest {
/**
* 进行自增操作的变量
* 使用volatile修饰
*/
private static volatile int count;
public static void main(String[] args) {
int threadNums = 2000;
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < threadNums; i++) {
service.execute(VolatileTest::addCount);
}
System.out.println(count);
service.shutdown();
}
private static void addCount() {
count++;
}
}
//输出结果
//1994
我们可以从例子中看出,共享变量使用了volatile修饰,启动2000个线程对其进行自增操作,如果是线程安全的,结果应该是2000;但结果却小于2000。证明volatile修饰的变量并不能保证原子性,如果想保证原子性,还需要额外加锁。
3.2、volatile禁止指令重排序
虽然程序从表象上看到是按照我们书写的顺序进行执行,但由于CPU可能会由于性能原因,对执行指令进行重排序,以此提高性能。
比如我们有一个方法是关于“谈恋爱”的方法。伪代码如下
{
//线程A执行1,2,3
//1、先认识某个女生,有好感
//2、开展追求
//3、追求成功
//线程B,等待线程A追求成功后开始进入甜蜜的爱情
while(!追求成功){
sleep();
}
//一起看电影,吃饭,牵手,接吻,xxx
}
我们看到线程A需要执行3步,由于cpu执行重排序优化,可能执行顺序变为1、3、2,乱套了,刚认识别人就成功了,接着就牵手,接吻,然后可能再执行追求的过程。。。。。。。。不敢想象,我还只是个孩子啊。这就是指令重排序可能在多线程环境下出现的问题。
如果我们使用volatile修饰“追求成功”的变量,则可以禁止CPU进行指令重排序,让谈恋爱是一件轻松而快乐的事情。
volatile使用内存屏障来禁止指令重排序。
在每个volatile写操作的前面插入一个StoreStore屏障,在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障,在每个volatile读操作的后面插入一个LoadStore屏障。
四、原子性、可见性、顺序性
我们看到JMM围绕这三个特征来建立的。
4.1、原子性
JMM提供了read、load、use、assign、store、write六个指令直接提供原子操作,我们可以认为java的基本变量的读写操作是原子的(long,double除外,因为有些虚拟机可以将64位分为高32位,低32位分开运算)。对于lock、unlock,虚拟机没有将操作直接开放给用户使用,但提供了更高层次的字节码指令,monitorenterm和monitorexit来隐式使用这两个操作,对应于java的synchronized关键字,因此synchronized块之间的操作也具有原子性。
4.2、可见性
我们上面说了线程之间的变量是隔离的,线程拿到的是主存变量的副本,更改变量,需要刷新回主存,其他线程需要从主存重新获取才能拿到变更的值。所有变量都要经过这个过程,包括被volatile修饰的变量;但volatile修饰的变量,可以在修改后强制刷新到主存,并在使用时从主存获取刷新,普通变量则不行。
除了volatile修饰的变量,synchronized和final。synchronized在执行完毕后,进行unlock之前,必须将共享变量同步回主内存中(执行store和write操作)。前面规则其中一条。
而final修饰的字段,只要在构造函数中一旦初始化完成,并且没有对象逃逸(指对象为初始化完成就可以被别的线程使用),那么在其他线程中就可以看到final字段的值。
4.3、有序性
有序性在volatile已经详细说明了。可以总结为,在本线程观察到的结果,所有操作都是有序的;如果多线程环境下,一个线程观察到另一个线程的操作,就说杂乱无序的。
java提供了volatile和synchronized两个关键字保证线程之间的有序性,volatile使用内存屏障,而synchronized基于lock之后,必须unlock后,其他线程才能重新lock的规则,让同步块在在多线程间串行执行。
五、Happends-Before原则
先行发生是java内存模型中定义的两个操作的顺序,如果说操作A先行发生于线程B,就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法等。
我们举个例子说一下。
//线程A执行
i = 1
//线程B执行
j = i
//线程C执行
i = 2
我们还是定义A线程执行 i = 1 先行发生于 线程B执行的 j = i;那么我们可以确定,在线程B执行之后,j的值是1。因为根据先行发生原则,线程A执行之后,i的值为1,可以被B观察到;并且线程A执行之后,线程B执行之前,没有线程对i的值进行变更。
这时候我们考虑线程C,如果我们还是保证线程A先行发生于B,但线程C出现在A与B之间,那么,你可以确定j的值是多少吗?答案是否定的。因为线程C的结果也可能被B观察到,这时候可能是1,也可能是2。这就存在线程安全问题。
在JMM下具有一些天然的先行发生关系,这些原则在无须任何同步协助下就已经存在,可以直接使用。如果两个操作之间的关系不在此列,并且无法从以下先行发生原则推导出来,它们就没有顺序性保证,虚拟机就会进行随意的重排序。
程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
锁定规则(Monitor Lock Rule):一个Unlock的操作肯定先于下一次Lock的操作。这里必须是同一个锁。同理我们可以认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来说是完全可见的。
volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作
线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
线程终止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必晚于线程中所有操作
线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生
对象终止规则(Finalizer Rule):一个对象的初始化方法先于执行它的finalize()方法
传递性(Transitivity):如果操作A先于操作B、操作B先于操作C,则操作A先于操作C
总结
本篇详细总结了Java内存模型。再来品一品这句话。
java内存模型规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
各位看官,如果觉得9龙的文章对你有帮助,求点赞,求关注。如果转载请注明出处。
本篇主要总结于:
深入理解Java虚拟机++JVM高级特性与最佳实践
你真的了解JMM吗?的更多相关文章
- 自己动手写把”锁”之---JMM和volatile
一.JAVA内存模型 关于Java内存模型的文章,网上真的数不胜数.在这里我就不打算说的很详细.很严谨了.只力求大家能更好的理解和运用,为后边的技术点做铺垫. 内存模型并不是Java独有的概念,而 ...
- Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)
JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...
- Java并发编程:JMM (Java内存模型) 以及与volatile关键字详解
目录 计算机系统的一致性 Java内存模型 内存模型的3个重要特征 原子性 可见性 有序性 指令重排序 volatile关键字 保证可见性和防止指令重排 不能保证原子性 计算机系统的一致性 在现代计算 ...
- (五)JMM的介绍
1. JMM的介绍 在上一篇文章中总结了线程的状态转换和一些基本操作,对多线程已经有一点基本的认识了,如果多线程编程只有这么简单,那我们就不必费劲周折的去学习它了.在多线程中稍微不注意就会出现线程安全 ...
- 多线程系列八:线程安全、Java内存模型(JMM)、底层实现原理
一.线程安全 1. 怎样让多线程下的类安全起来 无状态.加锁.让类不可变.栈封闭.安全的发布对象 2. 死锁 2.1 死锁概念及解决死锁的原则 一定发生在多个线程争夺多个资源里的情况下,发生的原因是 ...
- 6.你以为你真的了解final吗?
1. final的简介 final可以修饰变量,方法和类,用于表示所修饰的内容一旦赋值之后就不会再被改变,比如String类就是一个final类型的类.即使能够知道final具体的使用方法,我想对fi ...
- Java内存模型原理,你真的理解吗?
[51CTO.com原创稿件]这篇文章主要介绍模型产生的问题背景,解决的问题,处理思路,相关实现规则,环环相扣,希望读者看完这篇文章后能对 Java 内存模型体系产生一个相对清晰的理解,知其然知其所以 ...
- Java内存区域(运行时数据区域)和内存模型(JMM)
Java 内存区域和内存模型是不一样的东西,内存区域是指 Jvm 运行时将数据分区域存储,强调对内存空间的划分. 而内存模型(Java Memory Model,简称 JMM )是定义了线程和主内存之 ...
- 浅谈JMM
概述 JMM的全称是Java Memory Model(Java内存模型) JMM的关键技术点都是围绕着多线程的原子性.可见性和有序性来建立的,这也是Java解决多线程并行机制的环境下,定义出的一种规 ...
随机推荐
- csps模拟测试57
T1 天空龙 大神题,考察多方面知识,例如:快读 附上考试代码,以供后人学习 应某迪要求,我决定多写一点. 正如文化课有知识性失分和非知识性失分一样,OI也同样存在. 但非知识性失分往往比知识性失分更 ...
- Android开发中常用的设计模式
首先需要说明的是,这篇博文灵感来自于 http://www.cnblogs.com/qianxudetianxia/archive/2011/07/29/2121547.html ,在这里,博主已经很 ...
- 模拟示例raid 5(5块磁盘 3块做raid 2块做备份 ) raid 10(5块磁盘) 修改版
RAID5:需要至少三块(含)硬盘,兼顾存储性能.数据安全和储存成本. RAID10:需要至少四块(含)硬盘,兼具速度和安全性,但成本很高. raid 10(5块磁盘) 1.添加硬盘设备(添加5块) ...
- python经典算法题目:找出这两个有序数组的中位数
题目:找出这两个有序数组的中位数 给定两个大小为 m 和 n 的有序数组 nums1 和 nums2. 请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n)). 你可以 ...
- hashMapp
原文链接:https://www.iteye.com/topic/539465 Hashmap是一种非常常用的.应用广泛的数据类型,最近研究到相关的内容,就正好复习一下.网上关于hashmap的文章很 ...
- MySQL-配置环境变量及修改密码(附-mysql安装教程)
MySQL-配置环境变量和修改密码 mysql的安装教程:链接:https://pan.baidu.com/s/1rrPT2X0yRF58kN8jZZx-Mg 密码:55dh 一. 闪退问题 1.1. ...
- Ios第三方FMDB使用说明
SQLite (http://www.sqlite.org/docs.html) 是一个轻量级的关系数据库.iOS SDK很早就支持了SQLite,在使用时,只需要加入 libsqlite3.dyli ...
- PHP Swoole长连接常见问题
连接失效问题例子其中,Redis常见的报错就是: 配置项:timeout报错信息:Error while reading line from the serverRedis可以配置如果客户端经过多少秒 ...
- Android的系统框架
Android的系统架构采用了分层架构的思想,如图1所示.从上层到底层共包括四层,分别是应用程序程序层.应用框架层.系统库和Android运行时和Linux内核. 图1:Android系统架构图 每 ...
- tomcat启动窗口出现乱码
tomcat启动窗口出现乱码 或者 idea运行服务器tomcat出现乱码 在tomcat的启动窗口打印的启动信息中包含了大量的中文乱码, 虽然这些对tomcat本身的使用没有任何影响,但却非 ...