并发编程灵魂拷问系列之Java内存模型


并发编程灵魂拷问系列之Java内存模型

0.面试连环炮路径

JMM(Java内存模型)-> 原子性、可见性、有序性 -> volatile和可见性 -> volatile和有序性(指令重排+happens-before)-> volatile和原子性 -> volatile底层原理(内存屏障级别的原理)

1.聊一下Java的内存模型

volatile:共享变量修改时会强制刷新一下主内存的值

  1. 线程1/2 read 主内存的共享变量, load 到工作内存中,此时读到的值都为0
  2. 线程1/2 use 从工作内存中拿出共享变量,进行操作(比如 ++ 操作,那么计算之后的值都为1)
  3. 线程1/2 assign 把工作内存设置回工作内存中,之后工作内存尝试 store 写入主内存,写入成功就是 write
  4. 结果就是主内存data从 0 -> 1

2.你知道Java内存模型的原子性、有序性、可见性是什么

原子性:指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。比如 a = 1。

可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。Java保证可见性可以认为通过volatile、synchronized、final来实现。

有序性:程序执行的顺序按照代码的先后顺序执行,Java通过volatile、synchronized来保证。具备有序性,则不会发生指令重排

volatile禁止指令重排序,防止编译器编译优化时对代码重新排序,导致代码顺序变化。

【例子】正常情况是资源准备完毕,flag才设置为true,但是如图,如果发生指令重排,可能导致先设置为true再进行资源准备,导致直接直接execute而报错

3.讲讲volatile的工作原理,如何保证可见性?

volatile关键字用来保证 可见性有序性 的,不能保证原子性(极端特殊情况下可保证而已)

举例说明无法保证原子性(还是这张图):

就算使用volatile修饰data,当线程1更新data=1到主内存,使得线程二工作内存的data值失效,但是可能此时data已经在被运算中,及时失效了,还是会通过assign把data=1设置到工作内存中,最后write到主内存
需要保证原子性,还是需要 synchronizedlock 来保证

当volatile修饰值data,保证data在多线程之间的 可见性 ,如果data修改,会强制刷新其他线程工作内存中的data的值(让其他线程工作内存data的值失效,来起到强制刷新的作用)

volatile通过 禁止指令重排 来保证 有序性

4.你知道指令重排和happens-before是什么吗?

一般情况下,为了提高程序执行的效率,编译器或者指令器会对代码进行优化,例如指令重排。

java中有一个 happens-before 原则,一定程度上来避免胡乱地指令重排。他在一些特殊情况下,不允许编译器或者指令器对写的代码进行指令重排。

其中有一个与volatile相关的原则,如果代码中存在volatile修饰的值,此值进行volatile写后,再volatile读,那么顺序上必须先写再读

比如代码中,代码顺序A->B->C,那么A的顺序就优于C;或者比如对lock的操作顺序不能被重排(代码先unlock再lock,就不能重排为lock后unlock,否则导致流程错乱)

5.volatile是如何基于内存屏障保证可见性和有序性的?

内存屏障:禁止重排序

如何保证有序性
如果使用了volatile修饰一个值后,那么会对该值的读写前后会加入一些内存屏障,加入屏障之后,来避免发生指令重排

如何保证可见性
如果使用了volatile修饰一个值data后,在对data执行写操作,JVM会发送lock前缀指令给CPU,CPU收到指令,计算完毕会把data强制刷新到主内存里,其他线程通过对总线的嗅探,让工作内存的data值失效,之后读取data值时因为工作内存data值失效,所以从缓存中获取。保证了可见性

lock前缀指令 + MESI缓存一致性协议


评论
  目录