深入解析Java内存模型

一、背景

并发编程本质问题是:CPU、内存以及IO三者之间的速度差异。CPU速度快于内存、内存访问速度又远远快于IO,根据木桶理论,程序性能取决于最慢的操作,即IO操作。这样会出现CPU和内存交互时,CPU性能无法被充分利用,内存与IO交互时,内存性能也存在部分损耗,单方面提升CPU或内存的性能是无效的。为了提升CPU或内存的利用率,需要平衡三者的速度差异,计算机系统结构操作系统以及编译程序都做出了贡献,主要体现为:

1. 计算机体系结构:为CPU增加了缓存,均衡和内存的速度差异;
2. 操作系统:增加了进程和线程,便于分时复用CPU,均衡CPUIO设备的速度差异;
3. 编译程序:优化CPU指令的执行顺序,使得缓存被更加合理的利用。

上述方案虽然很大程度上解决了程序的性能问题,但也带来了许多隐藏的并发问题,主要为:可见性问题原子性问题以及有序性问题

可见性问题:

定义:一个线程对共享变量的修改,另外一个线程能够立刻看到,即可见性
导致原因:CPU缓存
详情解析:在多CPU时代,每个CPU都有自己的缓存,当多个CPU缓存同一份共享变量的数据时,线程A修改了共享变量,但修改目前只在CPU的缓存生效,其他CPU缓存还未来得及获取新修改的数据,线程B读取共享变量,读取的数据是老数据,存在数据不一致问题。

原子性问题:

定义:一个或多个操作在CPU执行过程中不被中断的特性,即原子性
导致原因:多线程上下文切换
详情解析:多线程底层执行是按照时间片来执行的,进行任务切换时,针对的是单个CPU指令,仅能保证单个CPU指令的原子性。而高级语言的一条语句,往往是由多个CPU指令完成。例如count += 1,至少需要三条指令:
1. 首先,将变量count加载到对应CPU的寄存器中;
2. 其次,在寄存器中执行 +1 操作;
3. 最后,将结果写入到内存中
若是线程A执行完指令1后,切换到线程B来执行 count += 1操作,线程B操作后count为2,但线程A中保存的count值仍是1,导致最后的结果为2,实际上应该为3。

Java中的原子操作有哪些:
1.longdouble之外的基本类型(int, byte, boolean, short, char, float)的赋值操作;针对long操作,直接拆分成两个32位的写入操作。
2. 所有引用reference的赋值操作;
3. java.concurrent.Atomic.*包中所有类的原子操作。
原子操作 + 原子操作 != 原子操作

【原子性对比:】
synchronized:不可中断锁,适合竞争不激烈,可读性好
Lock: 可中断,多样化同步,竞争激烈时能维持常态
Atomic: 竞争激烈时能维持常态,比Lock性能好;只能同步一个值

有序性问题:

定义:有序性是指按照代码的先后顺序来执行,但编译器为了优化性能,可能会改变代码的执行顺序。例如:i = 1; j = 2 变成 j = 2; i = 1
导致原因:编译优化
详情解析:在Java领域存在一个双重检查创建单例对象的场景,创建对象JVM底层分为三个步骤:
1. 分配内存空间;
2. 在内存上初始化对象;
3. 将对象地址赋值给实例变量
此时进行了编译优化,将1,2,3变成了1,3,2。那么就会出现多线程访问时,某些线程获取的对象为null,出现空指针问题。

因此,为了解决出现的可见性、原子性以及有序性问题,Java给出了一套解决方案,即Java Memory Model,简称JMM

二、Java内存模型

为了解决可见性和有序性,直观上可以理解:禁用缓存和编译优化,但程序的性能就无法保证。理想方案是:开发者按需禁用缓存和编译优化,因此Java做了两个方面的工作:

  1. 定义一种抽象计算机模型
  2. 定义一系列规则,来保证可见性和有序性。

2.1 定义

抽象计算机模型:JMM定义了线程和主内存之间的抽象关系:线程之间共享的变量存储在主内存中,每个线程有一个私有的本地内存,每个本地内存中存储了共享变量的副本。其中本地内存[工作内存]是一个抽象概念,底层对应着缓存、寄存器以及硬件和编译器优化等。主内存和工作内存之间的规范为:
在这里插入图片描述

  1. 所有的共享变量都存储于主内存:这里的变量值是实例变量、类变量以及数组,因为堆和方法区是线程共享的。(局部变量属于线程私有,不存在线程安全问题)
  2. 工作内存:每一个线程有自己的工作内存,工作内存中保留了被多个线程使用的变量的工作副本
  3. 线程不能直接读写主内存中的变量
    ① 只能操作自己的工作内存中的变量,
    ② 然后再同步到主内存中
  4. 工作内存的屏蔽性:不同线程之间不能直接访问对方工作内存中的变量,线程之间的值传递需要通过主内存来完成。(可见性问题的罪魁祸首)

一系列规则:volatile、synchronized和final三个关键字,以及六项Happens-Before规则。

2.2 规则

Happens-Before规则指的是:前面一个操作结果对后续操作是可见的。下面为详细的规则:

  1. 单线程规则:一个线程中的每个操作,happens-before于该线程的任意后续操作;
  2. 监视器锁规则(synchronized):对一个锁的解锁,happens-before于随后对这个锁的加锁;
  3. volatile变量规则:对一个volatile修饰的变量的写,happens-before于随后对这个变量的读;
  4. 传递性:如果A happens-before B、 B happens-before C、则A happens-before C;
  5. 线程start启动规则:主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B前的操作结果;
  6. 线程join()规则: 主线程A等待子线程B完成,当子线程B完成后,主线程能够看到子线程的操作结果。
  7. final规则: 通过final修饰变量,告诉编译器着变量是不会发生改变的,可以尽情优化。

三、总结

上述解决了可见性和有序性问题,原子性问题通过互斥锁可以完美解决。

  • 25
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

桃花猿

客官,赏点打酒钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值