【并发】volatile关键字面试整理
首先了解几个名词
JMM内存模型:
CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度
不匹配的矛盾
一次主内存的访问通常在几十到几百个时钟周期
一次L1高速缓存的读写只需要1~2个时钟周期
一次L2高速缓存的读写也只需要数十个时钟周期
可见性
由于t 线程频繁从主存读写
所以将其缓存到自己工作内存的高速缓存当中 减少对主存的访问
但对变量进行修改后 导致主内存中的代码没有被改动 其他线程无法感知修改
可见性问题解决方法
volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取
它的值,线程操作 volatile 变量都是直接操作主存
保证在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见
有序性
- synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是
synchronized 是属于重量级操作,性能相对更低 - happens-before 规定了对共享变量的写操作对其它线程的读操作可见
其中synchronized
线程解锁 对象M之前对变量的写,对于接下来对 对象M 加锁的其它线程对该变量的读可见
原子性
要么一起执行 要么一起失效
比如简单的自增i++语句
内部是需要进行 读取i 加一操作 写回内存 三步的
如果不保证原子性就回导致线程不安全
volatile无法保证原子性但是sychronized可以
volatile
现在进入正题来看volatile关键字
volatile关键字的作用?
- 作用1: 保证可见性 所有线程都可以看到共享变量的最新状态
保证变量的写
要么从高速缓存中写回主内存
要么高速缓存失效
必须从主内存读取
- 作用2: 禁止指令重排 保证有序性
- 场景也是他作用的一部分:
如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
volatile的底层是如何实现的?
底层有一个lock前缀指令
像一个屏障一样保证了可见性
lock前缀的功能具体表现为:
1.将当前处理器缓存行的数据会写回到系统内存。
2.这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
其中第二点就能保证其他CPU在处理这个共享变量的时候 意识到自己个人的高速缓存中已经是无效的了 于是就回去主存中更新获取
所以底层lock前缀指令解决了 写 和 读 两个问题保证了可见性
- 指令重排
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;(也就是上面所说的可以解决有序性问题的方法)
volatile变量的特性?
答:①可见性:对任意一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。②原子性:对任意单个volatile变量
的读/写具有原子性,但类似于i++这种复合操作不具有原子性
。
Q42:volatile变量的内存语义?
答:从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。从内存语义的角度来说,volatile的写-读与锁的释放-获取具有相同的内存效果。
volatile写的内存语义如下:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。volatile读的内存语义如下:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
线程A写一个volatile变量,实质上是线程A向接下来要读这个volatile变量的某个线程发出了(其对共享变量所修改的)消息。线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。线程A写一个volatile变量,线程B读一个volatile变量,实质上是线程A通过主内存向线程B发送消息。
Q43:volatile指令重排序的特点?
答:①当第二个操作是volatile写时,不管第一个操作是什么都不能重排序,这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
②当第一个操作是volatile读时,不管第二个操作是什么都不能重排序,这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
③当第一个操作是volatile写,第二个操作是volatile读时不能重排序。
Q44:volatile内存语义是怎么实现的?
答:JMM通过分别限制编译器重排序和处理器重排序来实现volatile的内存语义。编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此JMM采取保守策略。
Q45:JMM内存屏障插入策略有哪些?
答:①在每个volatile写操作之前插入一个Store Store屏障,禁止之前的普通写和之后的volatile写重排序。
②在每个volatile写操作之后插入一个Store Load屏障,防止之前的volatile写与之后可能有的volatile读/写重排序,也可以在每个volatile变量读之前插入该屏障,考虑到一般是读多于写所以选择用这种方式提升执行效率,也可以看出JMM在实现上的一个特点:首先确保正确性,然后再去追求效率。
③在每个volatile读操作之后插入一个Load Load屏障,禁止之后的普通读操作和之前的volatile读重排序。
④在每个volatile读操作之后插入一个Load Store屏障,禁止之后的普通写操作和之前的volatile读重排序。
Q46:JSR-133增强volatile内存语义的原因?
答:在旧的内存模型中,虽然不允许volatile变量之间重排序,但允许volatile变量与普通变量重排序,可能导致内存不可见问题。在旧的内存模型中volatile的写-读没有锁的释放-获取所具有的内存语义,为了提供一种比锁更轻量级的线程通信机制,严格限制了编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语义这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。
参考文章:
多线程并发之volatile的底层实现原理