java并发系列—JMM与先行先发原则详解

Java 内存模型(JMM)

​ 由于计算机的存储设备与处理器的运算能力之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中这样处理器就无需等待缓慢的内存读写了。

​ Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与 Java 编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。

​ Java 内存模型中规定了所有的变量都存储在主内存中(可以与物理硬件的内存类比),每条线程还有自己的工作内存(可以与前面将的处理器的高速缓存类比),线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成,
线程、主内存和工作内存的交互关系如下图所示。

image-20230115225652115

这里的主内存、工作内存与 Java 内存区域的 Java 堆、栈、方法区不是同一层次内存划分。(一个是JMM结构【上图所示】,一个是jvm结构)

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义了以下八种操作来完成,且这些必须是原子的、不可再分的

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

image-20230115225921427 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和 load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行 store 和 write 操作。Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。也就是 read 和 load之间,store 和 write 之间是可以插入其他指令的,如对主内存中的变量 a、b 进行访问时,可能的顺序是 read a,read b,load b, load a。Java 内存模型还规定了在执行上述八种基本操作时,必须满足如下规则image-20230115225921427

  • 不允许 read 和 load、store(存储) 和 write 操作之一单独出现
  • 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量。即就是对一个变量实施 use 和 store 操作之前,必须先执行过了assign 和 load 操作。
  • 一个变量在同一时刻只允许一条线程对其进行 lock 操作,lock 和 unlock 必须成对出现
  • 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值
  • 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去unlock 一个被其他线程锁定的变量。
  • 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write操作)。

补充(自己):synchronize无法禁止指令重排和处理器优化,为什么可以保证有序性可见性

  • 有序性:加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的
  • 可见性:线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中

happens-before (先行先发生原则)

​ 先行发生原则(Happens-Before)是判断数据是否存在竞争、线程是否安全的主要依据。先行发生是 Java 内存,模型中定义的两项操作之间的偏序关系,如果操作 A 先行发生于操作 B,那么操作 A 产生的影响能够被操作 B 观察到

Java 内存模型中存在的天然的先行发生关系

  1. 程序次序规则:一个线程内逻辑上书写在前面的操作先行发生于书写在后面的操作 ,因为多个操作之间有先后依赖关系,则不允许对这些操作进行重排序

  2. 锁定规则 :一个 unlock 操作先行发生于后面(时间的先后)对同一个锁的 lock 操作,所以线程解锁 m 之前对变量的写(解锁前会刷新到主内存中),对于接下来对 m 加锁的其它线程对该变量的读可见

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

  4. 传递规则 :具有传递性,如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C

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

    static int x = 10;//线程 start 前对变量的写,对该线程开始后对该变量的读可见
    new Thread(()->{ System.out.println(x);},"t1").start();
    
  6. 线程中断规则 (Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生可以通过 Thread.interrupt( )方法检测线程是否中断

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

  8. 对象终结规则(Finaizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始

总结:一个操作“时间上的先发生”不代表这个操作先行发生(时间上先调用线程一,再调用线程二,可能是线程二先发生【因为这其中没有先行先发原则在,随机调用顺序】);一个操作先行发生也不代表这个操作在时间上是先发生的(重排序的出现)

​ 时间上的先后顺序对先行发生没有太大的关系,所以衡量并发安全问题的时候不要受到时间顺序的影响,一切以先行发生原则为准

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值