【Java并发编程】Java内存模型
Java内存模型
一、JMM解析
之前写过一篇文章【Java核心技术卷】谈谈对Java平台的理解,其中讨论“Java跨平台”的篇幅占了大半的位置,JVM的重要性不言而喻。
为了能够屏蔽各种硬件以及对操作系统的内存访问的差异,而且要能使得Java程序在各个平台下都能达到一致的并发效果。JVM规范中定义了Java的内存模型(Java Memory model, JMM)。
JMM是一种规范,它规范了JVM与计算机内存是如何协同工作的,规定了一个线程如何以及何时看到其他线程修改过的共享变量的值以及在必须时如何同步地访问到共享变量。
下面我们来认识认识JMM,首先看一下规范下的JVM的内存分配。Heap为堆,Stack 为栈。
堆是一个运行时的数据区,也是Java的垃圾回收器重点关注的对象。堆的优势在于可以动态分配内存的大小,缺点是因为是运行时动态分配内存,存取速度要慢一点。
栈的优势存取的速度比堆要快,但是比计算机的寄存器要慢哈,栈的数据是可以共享的,但是存在栈中的数据的大小与生存期是确定的,灵活性较低,栈中主要存放一些基本类型的变量。比如int,short,long,byte,对象句柄等。
JMM要求调用栈和本地变量存放在线程栈上,对象存放在堆上。对了,一个对象可能包含方法,方法可能包含本地变量,这些本地变量仍然是存放在线程栈上的,即使这些对象拥有这些方法,也是要把这些对象放在堆中。
一个对象的成员变量随着对象存放在堆中,无论这些成员变量是原始类型还是引用类型。静态成员变量跟随类的定义一起存放在堆上,存放在堆上的对象可以被持有这个对象的引用的线程访问。线程通过引用访问这个对象的时候,也是能够访问这个对象的成员变量。如果多个线程同时调用同一个对象同一个方法,它们可能都将访问这个对象的成员变量,这个时候每个线程都会拥有相应的成员变量的私有拷贝
私有拷贝,有疑惑吗?我这里演示一下吧
public class Main { public static void main(String[] args) { new Thread(() -> { Person person1 = new Person(); person1.count(); System.out.println(person1.getId()); }).start(); new Thread(() -> { Person person2 = new Person(); person2.count(); System.out.println(person2.getId()); }).start(); } } class Person { private Long id = 0L; public void count(){ id++; } public Long getId() { return id; } }
结果:
上面是在JVM层次看多线程的。
下面看看硬件内存架构
二、硬件内存架构
上面展示的是多个CPU,有个概念,这里需要点一下,你可千万别搞混了
多核CPU指的是一个CPU有多个CPU核心,多核CPU性能非常好,但成本较高;如果没钱,可以换为多个单核的CPU,如果有钱可以换成多个多核的CPU。
现在我们使用的计算机大都都是多个多核CPU了,这使得在实际使用的时候,会有多个CPU上都跑的有进程(线程),我们的Java程序如果是并发的,可能会在多个CPU上跑。
我们看一下CPU的寄存器,每个CPU都包含一系列的寄存器,它们是CPU内存的基础,寄存器执行的速度远大于主存上执行的速度,中间的缓存,我就不多说了,上篇文章介绍过了【Java并发编程】CPU多级缓存
CPU从主存中读取数据的时候,首先会将数据读取到缓存中,然后由缓存读取到寄存器中,然后再去执行,执行完步骤后,如果需要将结果写回到主存中,首先要将数据刷新到缓存中,缓存会在未来的某个时间点,将结果刷新到主存中。
三、JMM与硬件内存架构的关联
Java内存模型与硬件架构模型之间是存在一些差异的,硬件架构模型没有区分线程栈与堆。对于硬件而言,所有的线程栈与堆都分配在主内存里面,部分线程栈和堆可能会出现在CPU的缓存中和CPU内部的寄存器中。
四、Java线程与计算机主内存之间的抽象关系
线程之间的共享变量存储在主内存里面,每一个线程都有一个私有的本地内存,本地内存是Java内存模型抽象的概念,并不是真实存在的,它涵盖了缓存、寄存器以及其他的硬件和编译器的优化等,本地内存中存储了该线程已读或写,共享变量拷贝的一个副本。Java内存模型的工作内存
是CPU的寄存器和高速缓存的一个抽象的描述。Java内存模型的存储划分仅是是对其内部的物理划分而已,只局限在JVM的内存。
由于每个线程都有自己的本地内存,它们如果同时访问主内存的共享变量,共享内存的值会分别copy到每个线程的本地变量中。每个线程对自己本地内存中的值做出的修改对其他线程都是不可见的,这个时候就会导致不一致性。
比如说主内存某个共享变量值为1,A和B线程都要对这个这个共享变量做出修改,A和B线程都先把值copy到自己的本地内存中,然后进行操作,A线程对其进行加1,并将值刷新到主内存中,B线程将其加2,但是相对于A线程慢了半拍,但是也成功将值刷新到主内存中。
此时,主内存中这个共享变量的值是3,当A再次从主内存中读取这个共享变量(中间会copy到它的本地内存),值已经不是2了。这个时候就导致了线程的安全性问题。
五、Java内存模型中同步八种操作
- lock(锁定):作用于主内存的变量,把—个变量标识为一条线程独占状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作內存的变量
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write的操作
- write(写入):作用于主内存的变量,它把 store操作从工作内存中一个变量的值传送到主内存的变量中
Lock作用于主内存的变量,它把一个变量标识为一个线程独占的状态,与其对应的就是unlock
Read读取,也是作用于主内存的变量,它变量的值从主内存变量输送到工作内存中(未到工作内存),与后边的load动作对接
Load是载入的意思,它将Read操作中变量的值放入工作内存的变量副本中
Use是使用,作用于工作内存中的变量, 它将工作内存中的变量传递给执行引擎,每当JVM遇到一个需要使用到的变量值的字节码指令的时候就会执行use这个操作。
Assign为赋值,作用于工作内存中的变量,它把从执行引擎接收到的值赋值给工作内存中的变量,每当JVM遇到一个需要给变量赋值的字节码指令的时候就会执行assign这个操作。
接下来是Store,也就是存储,它作用于工作内存中的变量,它将工作内存中的变量传递到主内存中(未到主内存),与后边的write操作对接
Write是写入的操作,它将Store操作中变量的值,放入到主内存的变量里面。
对应的同步规则有:
- 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行 store和 write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行
- 不允许read和load、 store和 write操作之一单独出现
- 不允许一个线程丢弃它的最近 assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
- 不允许一个线程无原因地(没有发生过任何 assign操作)把数据从工作内存同步回主内存中
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量。即就是对一个变量实施use和 store操作之前,必须先执行过了 assign和load操作
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的 unlock操作,变量才会被解锁。lock和 unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去 unlock—个被其他线程锁定的变量
- 对一个变量执行uηlock操作之前,必须先把此变量同步到主内存中(执行 store和 write操作)
Java并发相关的类设计时都遵循的规则,还有一些特殊的规则,之后再说。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Python入门无从下手?Python学习教程(知识架构)给你准备好了
我发现这两天,同学们对于python基础课程到底要学什么,一直不能很好的确定,虽然给大家出的Python学习教程和发给部分同学的视频资源,大家也都有看有学,但是很多同学还是不能有一个明确的方向,找不准自己的定位!那么我在这里针对python基础知识的几个阶段,做了一个总结,新手朋友们,可以根据这个来进行学习,那么由于我是做web开发的,所以会从web的角度去涉及。 第一阶段知识---基础语法 (1) python的数据结构的认识: python所有数据结构的认识 字符串的使用以及字符串的相关方法 列表的使用以及列表的相关方法 字典的使用以及字典的相关方法 元组的使用以及远足的相关方法 (2)逻辑判断的使用 if else if elif else (3)循环的使用 for 循环的使用 while循环的使用 continue 与 break 在循环里的使用 集合 set的使用 (4)函数的学习 函数的定义 函数的返回 (5)异常的学习 try except 的使用 raise 的学习 了解python内部的异常方法 (6)类的学习 类的创建,init的使用,类的实例化 继承的学习 (7)...
- 下一篇
开始报名!首次阿里巴巴经济体双 11 云原生实践展示
2019 天猫双 11 再次刷新世界纪录,订单创新峰值达到 54.4 万笔/秒,单日数据处理量达到 970PB。今年阿里巴巴核心系统 100% 上云,撑住了双 11 的世界级流量洪峰。 双 11 云原生专场- K8s & Cloud Native X Service Mesh Meetup 北京站旨在与开发者们分享阿里巴巴双 11 技术架构及落地实践经验。我们希望通过本场活动,让云原生能够真正走到开发者身边。 时间:11 月 24日 9:30 - 16:30 地点:北京市朝阳区大望京科技商务园区宏泰东街浦项中心B 座 2 层多功能厅 报名链接:http://www.huodongxing.com/event/4518626174911 嘉宾阵容 释放云原生价值,双 11 洗礼下的阿里巴巴 K8s 超大规模实践 【讲师简介】 曾
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- 设置Eclipse缩进为4个空格,增强代码规范
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS关闭SELinux安全模块
- CentOS8安装Docker,最新的服务器搭配容器使用
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS6,CentOS7官方镜像安装Oracle11G
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7