• 08. Java内存模型(JMM)


    1.JMM基础

    Java内存模型即Java Memory Model,简称JMMJMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。

    JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。Java1.5版本对其进行了重构,现在的Java仍沿用了Java1.5的版本。

    JMM遇到的问题与现代计算机中遇到的问题是差不多的: 物理计算机中的并发问题

    与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对虚拟机的实现也有相当大的参考意义。

    根据《Jeff DeanGoogle全体工程大会的报告》我们可以看到

    计算机在做一些我们平时的基本操作时,需要的响应时间是不一样的。

    (以下案例仅做说明,并不代表真实情况。)

    如果从内存中读取1Mint型数据由CPU进行累加,耗时要多久?

    做个简单的计算,1M的数据,Javaint型为32位,4个字节,共有1024*1024/4 = 262144个整数 ,则CPU 计算耗时:262144 *0.6 = 157 286 纳秒,而我们知道从内存读取1M数据需要250000纳秒,两者虽然有差距(当然这个差距并不小,十万纳秒的时间足够CPU执行将近二十万条指令了),但是还在一个数量级上。但是,没有任何缓存机制的情况下,意味着每个数都需要从内存中读取,这样加上CPU读取一次内存需要100纳秒,262144个整数从内存读取到CPU加上计算时间一共需要262144*100+250000 = 26 464 400 纳秒,这就存在着数量级上的差异了。

    而且现实情况中绝大多数的运算任务都不可能只靠处理器“计算”就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作是基本上是无法消除的(无法仅靠寄存器来完成所有运算任务)。早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

    在计算机系统中,寄存器划是L0级缓存,接着依次是L1L2L3(接下来是内存,本地磁盘,远程存储)。越往上的缓存存储空间越小,速度越快,成本也更高;越往下的存储空间越大,速度更慢,成本也更低。从上至下,每一层都可以看做是更下一层的缓存,即:L0寄存器是L1一级缓存的缓存,L1L2的缓存,依次类推;每一层的数据都是来至它的下一层,所以每一层的数据是下一层的数据的子集。

    在现代CPU上,一般来说L0L1L2L3都集成在CPU内部,而L1还分为一级数据缓存(Data CacheD-CacheL1d)和一级指令缓存(Instruction CacheI-CacheL1i),分别用于存放数据和执行数据的指令解码。每个核心拥有独立的运算处理单元、控制器、寄存器、L1L2缓存,然后一个CPU的多个核心共享最后一层CPU缓存L3

    Java内存模型(JMM

    从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

    1.可见性

    可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。

    要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。

    2.原子性

    原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

    我们都知道CPU资源的分配都是以线程为单位的,并且是分时调用,操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“时间片”。而任务的切换大多数是在时间片段结束以后,

    那么线程切换为什么会带来bug呢?因为操作系统做任务切换,可以发生在任何一条CPU 指令执行完!注意,是 CPU 指令,CPU 指令,CPU 指令,而不是高级语言里的一条语句。比如count++,在java里就是一句话,但高级语言里一条语句往往需要多条 CPU 指令完成。其实count++至少包含了三个CPU指令!

    3.volatile关键字

    同一时刻可以有很多线程操纵volatile修饰的数据, 在拿的时候是最新的,但其他线程也拿最新的并且对其更改,

    但如果同一时刻多个线程拿同一个数据对其进行更改,这样数据就不安全了所以volatile适用于一写多读的场景;

    1> volatile的可见性与原子性

    volatile只能保证可见性,复合操作时没有原子性

    1)可见性: 对一个volatile变量的读,总能看到(任意线程)对该volatile变量最后的写入; 一个线程修改变量了,其他线程可见

    2)原子性: 对任意单个volatile变量的读写具有原子性,但对于count++这种复合操作不具有原子性;volatile虽能保证执行完

    及时把这个变量刷到主内存中,但对于count++这种复合操作不具有原子性,由于线程切换,线程A刚count=0加载到工作内存,

    线程B就可以开始工作了,这样就导致线程A和B执行结果都是1,都写到主内存中,主内存的值还是1,不是2;

    2> volatile的实现原理

    volatile关键字修饰的变量进行写操作时,会使用CPU提供的Lock前缀指令,Lock会对CPU总线和高速缓存加锁(CPU指令级锁)

    同时该指令会将当前处理器缓存行的数据直接写回到系统内存中,这个写回操作使在其他CPU里缓存了该内存地址的数据无效;

    ARM架构三级流水线 重排序缓存 volatile 抑制重排序 适用场景: 一个线程写,多个线程读 或者 写的没有关联

    流水线 和 重排序 intel 十级流水线

    volatile保证不了线程安全,在DCL(双重检测锁定)的作用:

    jvm会出现指令的重排序,为了确保对象初始化完成,用volatile关键字防止指令重排序;

    java线程1---->线程栈内存(工作内存独享)<--拷贝共享变量--JVM控制-->+------+

    | save | count变量=0

    java线程2---->线程栈内存(工作内存独享)<--拷贝共享变量--JVM控制-->| 和 |<--->主内存(堆内存)

    | load | (RAM)

    java线程3---->线程栈内存(工作内存独享)<--拷贝共享变量--JVM控制-->+------+t1与t2修改后count变量=1(不安全)

    count+1=1 线程上下文切换时会出现数据混乱

    1. /**
    2. * 类说明:演示Volatile的提供的可见性 , 适合用于一写多读的情况
    3. */
    4. public class VolatileCase {
    5. private volatile static boolean ready; //如果不加 volatile ready 不保证可见性
    6. private static int number;
    7. private static class PrintThread extends Thread{
    8. @Override
    9. public void run() {
    10. System.out.println("printThread is running....");
    11. while (!ready);//无限循环
    12. System.out.println("number ="+number);
    13. }
    14. }
    15. public static void main(String[] args) {
    16. new PrintThread().start();
    17. SleepTools.second(1);
    18. number = 51;
    19. ready = true;
    20. SleepTools.second(5);
    21. System.out.println("main is ended!");
    22. }
    23. }

  • 相关阅读:
    python爬虫
    (1)攻防世界web-Training-WWW-Robots
    02 栈的原理与使用
    从零手写实现 nginx-25-directive map 条件判断指令
    JS逆向实战27——pdd的anti_content 分析与逆向
    2022全国大学生数学建模竞赛获奖难么?国赛求解过程技巧及方案
    TypeScript类的使用
    C#开发的OpenRA游戏之金钱系统(5)
    全球绿色建筑的 10 个最酷的例子
    【Java | 多线程】LockSupport 的使用和注意事项
  • 原文地址:https://blog.csdn.net/x910131/article/details/126023346