深入理解Java内存模型,小白也能看得懂!

对于物理机来说,高速缓存和主内存之间的交互有协议,同样的,Java内存中每个线程的工作内存和JVM占用的主内存的交互是由JVM定义了如下的8种操作来完成的,每种操作必须是原子性的。JVM中主内存和工作内存交互,就是一个变量如何从主内存传输到工作内存中,如何把修改后的变量从工作内存同步回主内存。

1)lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线程独占这个变量

2)unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定

3)read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用(解释:主内存–>工作内存,读取主内存)

4)load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)(解释:主内存–>工作内存,写入工作内存)

5)use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作(解释:工作内存–>执行引擎,变量操作)

6)assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作(解释:执行引擎–>工作内存,变量赋值)

7)store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用(解释:工作内存–>主内存,读取工作内存)

8)write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中(解释:工作内存–>主内存,写入主内存)

对于Java程序中的变量读取语句,要把一个变量从主内存传输到工作内存,就要顺序的执行read和load操作;

对于Java程序中的变量写入语句,要把一个变量从工作内存回写到主内存,就要顺序的执行store和write操作。

八条规则

对于普通变量,虚拟机只是要求顺序的执行,并没有要求连续的执行,所以如下也是正确的。对于两个线程,分别从主内存中读取变量a和b的值,并不一样要read a; load a; read b; load b; 也会出现如下执行顺序:read a; read b; load b; load a; 对于这8种操作,虚拟机也规定了一系列规则,在执行这8种操作的时候必须遵循如下的规则:

1)read和load、store和write:对于Java程序中的读取和写入,不允许read和load、store和write操作之一单独出现,也就是不允许从主内存读取了变量的值但是工作内存不接收的情况,或者不允许从工作内存将变量的值回写到主内存但是主内存不接收的情况

2)assign:对于Java程序中的执行引擎运算返回结果,不允许一个线程丢弃最近的assign操作,也就是不允许线程在自己的工作线程中修改了变量的值却不同步/回写到主内存

3)assign:不允许一个线程回写没有修改的变量到主内存,也就是如果线程工作内存中变量没有发生过任何assign操作,是不允许将该变量的值回写到主内存

4)use-load,store-assign:先后原则,变量只能在主内存中产生,源头必须是主内存,不允许在工作内存中直接使用一个未被初始化的变量,也就是没有执行load或者assign操作,也就是说在执行use、store之前必须对相同的变量执行了load、assign操作

5)lock:一个变量在同一时刻只能被一个线程对其进行lock操作,也就是说一个线程一旦对一个变量加锁后,在该线程没有释放掉锁之前,其他线程是不能对其加锁的,但是同一个线程对一个变量加锁后,可以继续加锁,同时在释放锁的时候释放锁次数必须和加锁次数相同。

6)lock:对变量执行lock操作,就会清空工作空间该变量的值,执行引擎使用这个变量之前,需要重新load或者assign操作初始化变量的值

7)unlock:不允许对没有lock的变量执行unlock操作,如果一个变量没有被lock操作,那也不能对其执行unlock操作,当然一个线程也不能对被其他线程lock的变量执行unlock操作

8)unlock:对一个变量执行unlock之前,必须先把变量同步回主内存中,也就是执行store和write操作

当然,最重要的还是如开始所说,这8个动作必须是原子的,不可分割的。

分解Java程序练习

常量读取零步操作,变量读取一步操作;

常量赋值一步操作,变量赋值两步操作;

常量计算并写入两步操作,变量计算并写入三步操作。

解释(八个原子性操作):

常量读取零步操作,啥都不干

变量读取一步操作,读取变量a,主内存–>工作内存,先读取主内存read,再写入工作内存load,根据下面规则1,两个不能拆开,所以变量读取是原子操作。

常量赋值一步操作,int a=1,工作内存–>主内存,先读取工作内存store,再写入主内存write,根据下面规则1,两个不能拆开,所以常量赋值给变量是原子操作。

变量赋值两步操作,int b=a,先变量a 主内存–>工作内存,然后变量b 工作内存–>主内存,两步操作。

常量计算并写入两步操作,int a=1+1,先 1+1=2 返回结果,执行引擎–>工作内存,使用assign命令,然后变量a 工作内存–>主内存,总共两步操作。

变量计算并写入三步操作,int b=a+1,先变量a 主内存–>工作内存,然后 a+1 返回结果,执行引擎–>工作内存,最后变量b 工作内存–>主内存,三步操作。

注意,先用int,先不考虑long和double,这两个64位的。

long double型变量的特殊规则

Java内存模型要求对主内存和工作内存交换的八个动作是原子的,正如上面所讲,但是对long和double有一些特殊规则。原因是什么呢?

其实,问题倒不是出现在8个动作上,这个8个动作是确实是原子性操作,这一点是毋庸置疑的,问题出在long和double这两种基本数据类型上。

八个动作中lock、unlock、read、load、use、assign、store、write对待32位的基本数据类型都是原子操作,对待long和double这两个64位的数据,java虚拟机规范对java内存模型的规定中特别定义了一条相对宽松的规则:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,也就是允许虚拟机不保证对64位数据的read、load、store和write这4个动作的操作是原子的。

这也就是我们常说的long和double的非原子性协定(Nonautomic Treatment of double and long Variables)。

原子性、可见性与有序性

===========

Java内存模型是围绕着并发过程中如何处理原子性、可见性和有序性3个特征建立的。

1)原子性:

由Java内存模型来直接保证原子性的变量操作包括read、load、use、assign、store、write这6个动作,虽然存在long和double的特例,但基本可以忽略不计,目前虚拟机基本都对其实现了原子性。如果需要更大范围的控制,lock和unlock也可以满足需求。

lock和unlock虽然没有被虚拟机直接开放给用户使用,但是提供了字节码层次的指令monitorenter和monitorexit对应这两个操作,对应到java代码就是synchronized关键字,因此在synchronized块之间的代码都具有原子性(这是程序员所熟知的)。

2)可见性:

可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。

除了volatile,synchronized和final也可以实现可见性。synchronized关键字是通过unlock之前必须把变量同步回主内存来实现的,final则是在初始化后就不会更改,所以只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。

3)有序性:

有序性从不同的角度来看是不同的。单纯单线程来看都是有序的,但到了多线程就会跟我们预想的不一样。可以这么说:如果在本线程内部观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句说的就是“线程内表现为串行的语义”,后半句指的是“指令重排序”现象和主内存与工作内存之间同步存在延迟的现象。

保证有序性的关键字有volatile和synchronized,volatile禁止了指令重排序,而synchronized则由“一个变量在同一时刻只能被一个线程对其进行lock操作”来保证。

总体来看,synchronized对三种特性(原子性、可见性、有序性)都有支持,虽然简单,但是如果无控制的滥用对性能就会产生较大影响。

synchronized关键字是绝对安全的,因为它可以同时保证原子性、可见性、有序性,但是这并不意味着synchronized关键字可以随意使用,事实上,synchronized是一种重量级锁,对性能的影响还是比较大的,本文第五部分介绍锁优化就是为了解决synchronized重量级锁的性能损耗问题。

有序性:先行发生原则

==========

有序性:八条先行发生原则

Java内存模型具备一些 先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性 ,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

下面就来具体介绍下happens-before原则(先行发生原则):

(1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

(2)锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;

(3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

(4)传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;

(5)线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;

(6)线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

(7)线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

(8)对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

由于文案过于长,在此就不一一介绍了,这份Java后端架构进阶笔记内容包括:Java集合,JVM、Java并发、微服务、SpringNetty与 RPC 、网络、日志 、Zookeeper 、Kafka 、RabbitMQ 、Hbase 、MongoDB、Cassandra 、Java基础、负载均衡、数据库、一致性算法、Java算法、数据结构、分布式缓存等等知识详解。

image

本知识体系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。

image

image

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
本知识体系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。

[外链图片转存中…(img-7OI6zliI-1712485509853)]

[外链图片转存中…(img-S1dQMOzT-1712485509853)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值