1、被volatile修饰的变量有2大特点
1.1、可见性
1.2、有序性(有时需要禁止重排)
排序要求,有时需要禁止重排
1.3、volatile的内存语义
当写一个volatile
变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatle
变量时,JMM会把该线程对应的**本地内存设置为无效,重新回到主内存中读取最新共享变量****
所以volatile
的
写内存语义
- 是直接刷新到主内存中,
读的内存语义
- 是直接从主内存中读取。
1.4、凭什么可以保证可见性和有序性
内存屏障Memory Barrier
2、内存屏障(面试重点)
2.1、生活case
没有管控,顺序难保
设定规则,禁止乱序
2.2、再说volatile两大特性
- 可见性
写完后立即刷新回主内存并及时发出通知,大家可以去主内存拿最新版,前面的修改对后面所有线程都是可见的
- 有序性(禁重排)
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
不存在数据依赖关系,可以重排序;
存在数据依赖关系,禁止重排序
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
2.3、是什么
内存屏障︰是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。
也叫内存栅栏或栅栏指令
内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU
或编译器在对内存随机访问的操作中的一个同步点,使得此点之前
的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则
会要求Java
编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatle
实现了Java
内存模型中的可见性和有序性
(禁重排),但volatile
无法保证原子性。
内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
写屏障(Store Memory Barrier
):
- 告诉处理器在写屏障之前将所有存储在缓存
(store bufferes
)中的数据同步到主内存。 - 也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行
读屏障(Load Memory Barrier
):
- 处理器在读屏障之后的读操作,都在读屏障之后执行。
- 也就是说在
Load
屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。
一句话:
- 对一个
volatle
变量的写,先行发生于任意后续对这个volatile
变量的读,也叫写后读。
.2.4、内存屏障分类
2.4.1、一句话
上一章讲解过happens-before
先行发生原则,类似接口规范,
落地?落地靠什么?
2.4.2、粗分2种
读屏障(Load Barrier
)
在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据
写屏障(Store Barrier
)
在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
2.4.3、细分4种
C++源码分析
IDEA工具里面找Unsafe.class
Unsafe.java
Unsafe.cpp
OrderAccess.hpp
orderAccess_linux_x86.inline.hpp
orderAccess_linux_x86.inline.hpp
四大屏障分别是什么意思
2.5、什么保证有序性?(禁重排)
通过内存屏障禁重排
1、重排序有可能影响程序的执行和实现。因此,我们有时候希望告诉JVM你别“自作聪明”给我重排序,我这里不需要排序,听主人的。
2、对于编译器的重排序,JMM会根据重排序的规则,禁止特定类型的编译器重排序。
3、对于处理器的重排序,Java编译器在生成指令序列的适当位置,插入内存屏障指令,来禁止特定类型的处理器排序。
2.6、happens-before之volatile变量规则
当第一个操作为volatile
读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatle
读之后的操作不会被重排到volatile读之
前。
当第二个操作为volatile
写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatle
写之前的操作不会被重排到volatie
写
之后。
当第一个操作为volatile
写时,第二个操作为volatile
读时,不能重排。
2.7、JMM就将内存屏障插入策略分为4种规则!!
读屏障
在每个volatile读操作的后面插入一个LoadLoad
屏障
在每个volatile读操作的后面插入一个LoadStore
屏障
volatile的读。后面的普通读写可能用到,不允许重排
指令序列示意图
写屏障
在每个volatile 写操作的前面插入一个StoreStore
屏障
在每个volatile写操作的后面插入一个StoreLoad
屏障
volatile的写,可能是由前面的普通读写的来的,所有不允许排到后面
3、volatile特性
3.1、保证可见性
3.1.1、说明
保证不同线程对某个变量完成操作后结果及时可见,即该共享变量一旦改变所有线程立即可见
3.1.2、Code
不加volatile
,没有可见性,程序无法停止
加了volatile
,保证可见性,程序可以停止
public class VolatileSeeDemo {
static boolean flag = true;
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t ------come in");
while (flag){
}
System.out.println(Thread.currentThread().getName()+"\t ------flag被设置为false,程序停止");
},"t1").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
flag = false;
System.out.println(Thread.currentThread().getName() + "\t 修改完成");
}
}
线程启动后会将flag
复制一份到自己的栈帧里,所以main
线程修改自己栈帧里的flag
后刷新到主存,并不会影响到t1
里的flag
public class VolatileSeeDemo {
// static boolean flag = true;
static volatile boolean flag = true;
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t ------come in");
while (flag){
}
System.out.println(Thread.currentThread().getName()+"\t ------flag被设置为false,程序停止");
},"t1").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
flag = false;
System.out.println(Thread.currentThread().getName() + "\t 修改完成");
}
}
3.1.3、上述代码原理解释
线程t1
中为何看不到被主线程main
修改为false
的flag
的值?
问题可能:
-
1 主线程修改了
flag
之后没有将其刷新到主内存,所以t1线程看不到。 -
⒉主线程将flag刷新到了主内存,但是
t1
一直读取的是自己工作内存中flag
的值,没有去主内存中更新获取flag
最新的值。
我们的诉求:
-
1.线程中修改了自己工作内存中的副本之后,立即将其刷新到主内存;
-
2.工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
解决
使用volatile
修饰共享变量,就可以达到上面的效果,被volatile
修改的变量有以下特点:
1.线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
2.线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
3.1.4、 v o l a t i l e 变 量 的 读 写 过 程 ! ! \color{red}volatile变量的读写过程!! volatile变量的读写过程!!
Java内存模型中定义的8种每线程自己的工作内存与主物理内存之间的原子操作
加锁后会清空所有工作内存变量的值
在使用变量前必须重新load或assign
read
:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
load
:作用于工作内存,将read
从主内存传输的变量值放入工作内存变量副本中,即数据加载
use
:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
assign
.:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
store
:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
write
:作用于主内存,将store
传输过来的变量值赋值给主内存中的变量
由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令
lock
:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
unlock
:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
3.2、没有原子性
class MyNumber{
volatile int number;
public void addPlus(){
number ++;
}
}
public class VolatileAtomicDemo {
public static void main(String[] args) {
MyNumber myNumber = new MyNumber();
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myNumber.addPlus();
}
},String.valueOf(i)).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(myNumber.number);
}
}
volatile
只保证读取时读取的是最新的。要修改这个值的时候,发现了主内存数据已经更新指令(其他线程已经将修改的数据提交到主内存),这个时候就会放弃修改(写丢失问题),然后重新读操作(上述代码相当于此次循环作废)
对于volatle
变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅是数据加载时是最新的。但是多线程环境
下,“数据计算”"和"数据赋值"操作可能多次出现,若数据在加载之后,
若主内存volatile
修饰变量发生修改之后,线程工作内存中的操作将会作废去读主内存最新值,操作出现写丢失问题。
即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。
由此可见volatile
解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。
3.2.1、从i++的字节码角度说明
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
public void add(){
i++;//不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分3步完成
}
如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同
值的加1
操作,这也就造成了线程安全失败,因此对于add
方法必须使用synchronized
修饰以便保证线程安全.
3.2.2、结论
volatile
变量不适合参与到依赖当前值的运算
volatile
变量不适合参与到依赖当前值的运算,如i=i+ 1; i++;
之类的
那么依靠可见性的特点volatile可以用在哪些地方呢?
通常volatile
用做保存某个状态的boolean
值or int
值。
JVM的字节码,i++
分成三步,间隙期不同步非
原子操作(i++)
对于volatle
变量,JVM
只是保证从主内存加载到线程工作内存的值是最新的,也只是数据加载时是最新的。
如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,也就造成了线程安全问题。
3.3、指令禁重排!!
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
不存在数据依赖关系,可以重排序;
存在数据依赖关系,禁止重排序
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
编译器优化的重排序:
- 编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
指令级并行的重排序:
- 处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执
行顺序内存系统的重排序:
- 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性。
若存在数据依赖关系,禁止重排序===>重排序发生,会导致程序运行结果不同。
编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在依赖关系的两个操作的执行;但不同处理器和不同线程之间的数据性不会
被编译器和处理器考虑,其只会作用于单处理器和单线程环境,下面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被
改变。
读屏障
在每个volatile读操作的后面插入一个LoadLoad
屏障
在每个volatile读操作的后面插入一个LoadStore
屏障
volatile的读。后面的普通读写可能用到,不允许重排
指令序列示意图
写屏障
在每个volatile 写操作的前面插入一个StoreStore
屏障
在每个volatile写操作的后面插入一个StoreLoad
屏障
volatile的写,可能是由前面的普通读写的来的,所有不允许排到后面
3.3.1、code说明
多线程中,不加volatile
可能输出的是0
,因为指令重排序
4、如何使用volatile
4.1、单一赋值可以,但是含复合运算赋值不可用(i++之类)
4.2、状态标志,判断业务是否结束
4.3、开销较低的读,写锁策略
4.4、DCL双端锁的发布
4.4.1、问题
单线程环境下(或者说正常情况下),在"问题代码处",会执行如下操作,保证能获取到已完成初始化的实例
由于存在指令重排序
多线程问题代码
隐患:多线程环境下,在"问题代码处",会执行如下操作,由于重排序导致2,3乱序,后果就是其他线程得到的是未初始化的对象而不是完成初始化的对象
外面的检测没有synchronized
。另一个线程让instance
指向开辟好的但是为初始化的空间,此时线程切换,就出现了别的线程拿到了未初始化的实例
4.4.2、解决
5、volatile小总结
5.1、volatile可见性
1.线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
2.线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
详细看5.3.1.4
5.2、volatile没有原子性
详细 5.3.3
5.3、volatile禁重排
详细看5.2.7和5.3.3
5.3.1、写指令
5.3.2、读指令
5.4、凭什么我们写了一个volatile关键字,系统底层加入内存屏障?两者关系?
5.4.1、字节码层面
5.5、内存屏障是什么
内存屏障︰是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。
也叫内存栅栏或栅栏指令
5.6、内存屏障能干嘛
阻止屏障两边的指令重排序
写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据
5.7、内存屏障四大指令
在每一个volatile写操作前面插入一个StoreStore
屏障
在每一个volatile写操作后面插入一个StoreLoad
屏障
在每一个volatile读操作后面插入一个LoadLoad
屏障
在每一个volatile读操作后面插入一个LoadStore
屏障
5.7、3句话总结
volatile 写之前的操作,都禁止重排序到 volatile 之后
volatde 写之后的操作,禁止重排序
volatile读之后的操作,都禁止重排序到volatile之前