java并发编程(4)-----内存模型

一、什么是内存模型

假设一个线程为一个变量a赋值:a = 1;

内存模型需要解决这个问题:“在什么条件下,读取a的线程将看到这个值为3”,在单线程情况下,不会发生任何问题,任何条件下,看到的都是3。但是在多线程情况下,如果没有同步,就有可能使得某个线程无法立即甚至永远看不到另一个线程对某个变量做出的更改。

这是因为编译器生成的指令顺序可能与源码中的顺序不同,编译器还会把一些变量保存到寄存器而不是内存中;CPU也会进行指令重排序来加快执行速度,而且CPU的本地缓存(L1、L2)对于其他处理器也是不可见的,这些都是可能造成一个线程的更改无法被另外一个线程立马感知到的原因。

为什么会出现这些情况呢?这是因为JVM规定:只要程序的最终结果与严格串行执行的结果一致,那么所有操作都是允许的。所以,为了提高效率,才有了上述的一些优化。CPU的本地缓存是为了减少读主存的频次,提高效率。

所以当多个线程共享一个数据时,为了保证可见性,java内存模型规定了JVM必须遵守的一组最小保证:即对变量的写入操作在何时将对其他线程可见。

二、java内存模型

JMM是为了明确在多线程场景下,什么时候可以重排序,什么时候不能重排序所引入的规范。对上,是JVM和开发者之间的协定,对下,是JVM和编译器、CPU之间的协定。

JMM是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放,线程的启动与合并。

JMM为程序中所有的操作定义了一个偏序关系,称之为happens-Before,例如要想保证执行操作B的线程看到操作A的结果(无论A,B是否由同一个线程执行),那么A和B之间必须满足Happens-Before关系,如果两个操作间没有Happens-Before关系,那么JVM可以对其任意重排序。

Happens-Before规则包括:

  • 程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作将在B操作前执行。
  • 监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
  • volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。
  • 线程启动规则:在线程上对Thread.Start的调用必须在该线程中执行任何操作之前。
  • 线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行。(其他线程检测该线程结束:或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false)
  • 中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行。
  • 终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
  • 传递性:如果操作A在操作B之前,操作B在操作C之前,那么操作A必须在操作C之前执行。

除此之外,我们还可以通过这些机制构造更复杂的Happens-Before规则,也可以自己提供一些这样的规则。JMM规定的只是最小的要求。

例如在JUC包中就提供一些其他的HB规则:

  • 将一个元素放入一个线程安全容器的操作将在另一个线程从该容器中获得这个元素的操作之前执行
  • 在CountDownLatch上的倒数操作将在线程从闭锁上的await方法中返回之前执行。
  • 释放Semaphore许可的操作将在从该Semaphore上获得一个许可之前执行。
  • Future表示的任务的所有操作将在从Future.get中返回之前执行
  • 向Executor提交一个Runable或者Callable的操作将在任务开始执行之前执行。
  • 一个线程到达CyclicBarrier或Exchanger的操作将在其他到达该栅栏或交换点的线程被释放之前执行。

三、OS的内存模型

缓存一致性协议,例如MESI协议。此外还定义了一些特殊的指令,即内存屏障,当需要共享数据时,这些指令就能实现额外的存储协调保证。

3.1、CPU与缓存一致性

按照数据读取顺序和与CPU结合的紧密程度,CPU缓存可以分为一级缓存(L1),二级缓存(L2),部分高端CPU还具有三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。
单核CPU只含有一套L1,L2,L3缓存;如果CPU含有多个核心,即多核CPU,则每个核心都含有一套L1(甚至和L2)缓存,而共享L3(或者和L2)缓存。

  • 单线程:cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。
  • 单核CPU,多线程:进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。
  • 多核CPU,多线程:每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的cache中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。

为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。

3.2 MESI协议

MESI协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

MESI协议规定CPU的每个缓存行用4种状态进行标记(M、E、S、I)

  • M(Modified,被修改):该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。
  • E(Exclusive,独享的):该缓存行只被缓存在该CPU内存中,且是未被修改过的(clean),与主存数据一致。该状态在任何时刻当有其他CPU读取该内存时变为共享状态(Shared)。同时,当该CPU修改这个缓存行内容时,该状态修改为Modified。
  • S(Shared,共享的):该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行的内容时,其他CPU中缓存的这个缓存行变为Invalid(无效的)。
  • I(Invalid,无效的):表示CPU缓存的这个缓存行无效,如果需要使用这个缓存行中的数据,需要重新从主存读取。

四、Java内存模型与OS内存模型的关系

JMM是语言级的内存模型,通过JVM提供的一些内存屏障指令在编译期间保证指令的顺序,OS的内存是硬件上的内存模型,JVM的指令最终都会化作硬件上的内存屏障等,最终保证程序的正确执行。

Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。即JMM在更高的一个层面进行一些控制防止并发时候出现错误,OS内存模型则是更底层,JMM在上层进行控制的时候可能会使用一些底层OS内存模型提供的功能,比如内存屏障。

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:

  • 线程之间的共享变量存储在主内存(Main Memory)中
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
  • 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
  • Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。

在这里插入图片描述

参考:https://zhuanlan.zhihu.com/p/29881777

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值