前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM暴力突破之JMM内存模型

JVM暴力突破之JMM内存模型

作者头像
我就是马云飞
发布2021-02-26 16:16:16
5750
发布2021-02-26 16:16:16
举报
文章被收录于专栏:我就是马云飞

java内存划分

JMM规定了内存主要划分为主内存和工作内存两种。此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的层次上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。具体如图所示:

image

正常情况下,java中所有的数据都是放在主存中的,如图所示:

image

随着 CPU 技术的发展,CPU的执行速度越来越快,但内存的技术并没有太大的变化,如果JAVA线程每次读取和写入变量都直接操作主内存,对性能影响比较大,所以每条线程拥有各自的工作内存,工作内存中的变量是主内存中的一份拷贝,线程对变量的读取和写入,直接在工作内存中操作,而不能直接去操作主内存中的变量。

因此,为了“压榨”处理性能,达到“高并发”的效果,在CPU中添加了高速缓存(cache)来作为缓冲。

image

但是这样就会出现一个问题,当一个线程修改了自己工作内存中变量,对其他线程是不可见的,会导致线程不安全的问题。也就是缓存一致性的问题。

缓存一致性

现在市面上的手机通常有两个或者多个CPU,其中一些CPU还有多核。每个CPU在某一时刻都能运行一个线程,这就意味着,如果你的Java程序是多线程的,那么就有可能存在多个线程在同一时刻被不同的CPU执行的情况。

指令重排

除了缓存一致性问题,还存在另外一种硬件问题,也比较重要:为了使CPU内部的运算单元能够尽量被充分利用,处理器可能会对输入的字节码指令进行重排序处理,也就是处理器优化。除了CPU之外,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做指令重排。

比如如下代码:

代码语言:javascript
复制
int a = 1;
int b = 1;
a = a + 1;

经过CPU指令重排后,可能会变成如下代码:

代码语言:javascript
复制
int a = 1;
a = a + 1;
int b = 1;

如果我们任由 CPU 优化或者编译器指令重排,那我们编写的Java代码最终执行效果可能会极大的出乎意料。为了解决这个问题,让Java代码在不同硬件、不同操作系统中,输出的结果达到一致,Java 虚拟机规范提出了一套机制——Java 内存模型。

什么是java内存模型

JMM (Java Memory Model)是Java内存模型,JMM定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节。解决了CPU多级缓存、CPU 优化、指令重排等导致的内存访问问题,从而保证Java程序(尤其是多线程程序)在各种平台下对内存的访问效果一致。

内存交互操作

内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)

  • lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
  • unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
  • use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
  • assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
  • store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
  • write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

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操作之前,必须把此变量同步回主内存

Happens-before 先行发生原则

先行发生原则用于描述2个操作之间的内存可见性。意思就是当A操作先行发生于B操作,则在发生B操作的时候,操作A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。

JMM 中定义了以下几种情况是自动符合 happens-before 规则的:

  • 程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
  • 锁定规则(Monitor Lock Rule):无论是在单线程环境还是多线程环境,一个锁如果处于被锁定状态,那么必须先执行 unlock 操作后才能进行 lock 操作。
  • volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作
  • 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。假定线程 A 在执行过程中,通过执行 ThreadB.start() 来启动线程 B,那么线程 A 对共享变量的修改在线程 B 开始执行后确保对线程 B 可见。
  • 线程中止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必须晚于线程中所有操作
  • 线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生
  • 对象中止规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法
  • 传递性(Transitivity):如果操作A先于操作B、操作B先于操作C,则操作A先于操作C

总结

  • Java 内存模型的来源:主要是因为CPU缓存和指令重排等优化会造成多线程程序结果不可控。
  • Java 内存模型是什么:本质上它就是一套规范,在这套规范中有一条最重要的 happens-before 原则。

参考

拉勾教育 - Android 工程师进阶34讲(文中图片均来自第七讲)


代码人生,一飞冲天。

END

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-01-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农职场 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • java内存划分
  • 缓存一致性
  • 指令重排
  • 什么是java内存模型
  • 内存交互操作
  • Happens-before 先行发生原则
  • 总结
  • 参考
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档