JUC(四)-Java内存模型之JMM

本文最后更新于:2024年4月22日 下午

Java内存模型下对变量的规则,多线程下变量赃读取脏写的原因,内存屏障及其原理分析,以及Volatile的关键字的作用,原理。使用场景

Java并发多线程与高并发(二)-并发基础 已经对概念进行的基础的覆盖,接下来逐步加深

多线程先行发生原则之happens-before

happens-before之8条

  • 程序次序规则(Program Order Rule):写在前面的操作先行发生于写在后面的操作
  • 管程锁定规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作
  • volatile 变量规则(Volatile Variable Rule):对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的。这里的 “后面” 同样是指时间上的先后。
  • 传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。
  • 线程启动规则(Thread Start Rule):Thread 对象的 start() 方法先行发生于此线程的每一个动作。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread 对象的 interrupted() 方法检测到是否有中断发生。
  • 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测。我们可以通过 Thread 对象的 join() 方法是否结束、Thread 对象的 isAlive() 的返回值等手段检测线程是否已经终止执行。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。

Volatile内存屏障(难点)

并发编程的三大问题:原子性 可见性 有序性

volatile 保证了可见性和有序性

volatile:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主存

当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主存中读取共享变量

所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取

内存屏障(保证了可见性)

内存屏障指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令

对一个volatie域的写,happens-before与任意后续对这个volatile域的读,也叫写后读

底层是C++

loadload storestore loadstore storeload 四个内存屏障策略

屏障类型 指令示例 说明
loadload load1;loadload;load2 保证load1读取操作在load2读取操作之前执行
storestore store1;storestore;store2 在store2及其后的写操作执行前,保证store1的写操作已经刷新到主内存
loadstore load1;storestore;store2 在store2及其后的写操作执行前,保证load1的读操作已读取结束
storeload store1;storestore;load2 保证store1的写操作已刷新到主内存之后,load之后的读操作才能执行

总结:(重点)

1、当第一个操作Volatile读时候,无论第二个操作是什么,都不能重排序,保证Volatile读之后的操作不会重排序到Volatile读之前

2、当第二个操作是Volatile写时候,无论第一个操作是什么,都不能重排序,保证Volatile写之前的操作不会被重排序到Volatile写之后

3、当第一个操作是Volatile写时,第二个操作是Volatile读时,不能重排

延伸:使用两个Volatile时候,只有第一个是Volatile读,第二个也是Volatile读才可以重排,其他所有情况均不可以重排

volatile

写总结:

1、在每个volatile写操作的前面插入一个storestore屏障

2、在每个volatile写操作的后面插入一个storeload屏障

读总结

1、在每个volatile读操作后面插入一个loadload屏障

2、在每个volatile读操作后面插入一个loadstore屏障

Volatile变量的读写过程

read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)

read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存。

laod:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载。

use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作。

assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作。

write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量。

由于上述操作,只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以JVM另外提供了两个原子指令:

lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。

unlock:作用于主内存,将一个处于锁定状态的变量释放,然后才能被其它线程占用。

Volatile 为什么没有原子性?

复合性的操作不具有原子性

example:i++

i++ 被拆分处理三个指令:

1、getfield拿到原始i

2、执行iadd进行加一操作

3、执行putfield把累加后的值写回

如果第二个线程在第一个线程读取旧值和写回新值期间读取了i的值,那么第二个线程就会和第一个线程看到同一个值,并执行加一操作,导致线程安全失败。

本质原因

内存屏障保证了read(读取)→load(加载)→use(使用) 以及assign(赋值)→store(存储)→write(写入)成为了两个不可分割的原子操作,但是在use和assign之间依然有真空期,这个时候如i++ 这样多个操作就会导致多线程安全问题

Volatile的应用场景

通常用作保存某个boolean 或者 int 值


JUC(四)-Java内存模型之JMM
https://hyq965672903.gitee.io/posts/f189a462.html
作者
灼华
发布于
2022年6月18日
许可协议