volatile 翻译:易变的;(计算机内存)易失的
volatile是java虚拟机提供的轻量级的同步机制;
被volatile修饰的变量的3大特点:
1.保证可见性 (可见性)
2.不保证原子性 (不保证原子性)
3.禁止指令重排 (有序性)
前置知识补充:什么是 JMM模型 (Java内存模型 Java Memory Model)
Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝。
volatile的内存语义
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立刻刷新回主内存中
2.当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
所以volatile的 写内存语义 是直接刷新到主内存中, 读的内存语义 是直接从内存中读取
1.可见性
保证不同线程对这个变量进行操作时的可见性,即变量一旦改变对所有线程立即可见
public class VolatileSeeDemo {
// static boolean flag = true; // 不加volatile,没有可见性
static volatile boolean flag = true; // 加volatile,保证可见性
public static void main(String[] args) {
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "---come in");
while (flag){
new Integer(308);
}
System.out.println("t1 over");
},"t1").start();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() ->{
flag = false;
},"t2").start();
}
}
问题引出:
不加volatile关键字的时候,线程t1为什么看不到被线程t2修改为flase的flag的值(flag=false为什么没生效)
问题可能:
1.线程2修改了flag值之后,没有将其刷新到主内存,所以t1线程看不到
2.主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中的flag的值,没有去主内存中更新获取flag的最新的值
2.没有原子性
volatile变量的复合操作(如i++)不具有原子性
public class Test01{
public volatile int i;
public void add(){
i++;
}
}
其中: i++ 被拆分成了3个指令:
1.执行getfiled拿到原始i;
2.执行iadd进行加1操作;
3.执行putfield写,把累加后的值写回
分析:如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并且执行相同值的加1操作,造成了线程安全失败; 因此,对add方法必须使用synchronized修饰,以保证线程安全
3.指令禁重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下3种
单线程环境中可以确保程序最终执行结果和代码顺序执行的结果一致
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保持一致是无法确定的;
重排序:是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序,不存在数据依赖关系,可以重排序,存在数据依赖关系,禁止重排序
但是重排序后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
重排序的分类和执行流程:
编译器优化重排序: 编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序
指令级并行重排序: 处理器使用指令集并行技术来将多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性
public class ResortSeqDemo{
int a = 0;
boolean flag = false;
public void method01(){
a = 1;
flag = true;
}
public void method02(){
if(flag){
a = a + 5;
System.out.println("result value " + a)
}
}
}
多线程环境中,由于编译器优化重排的存在,两个线程中使用的变量能否保持一致性是无法确定的,结果无法预测
使用场景
结论:由于volatile变量只能保证可见性,在不符合以下2条规则的运算场景中,我们仍然要通过加锁(synchronized,java.util.concurrent中的锁或者原子类)来保证原子性:
1 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2 变量不需要与其他的状态变量共同参与不变约束
理解:上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行
如何正确的使用volatile
1.单一的赋值可以,但是含复合运算赋值不可以(i++之类)【正确示例:volatile int a=10; volatile boolean flag = false】
2.状态标志位,判断业务是否结束
3.开销较低的读,写锁策略
4.DCL双端锁的发布
内存屏蔽
volatile凭什么可以保证可见和有序,依靠的就是内存屏障,即volatile的底层原理。
内存屏障(是一类同步屏障指令,是CPU或者编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免了代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了java内存模型中的可见性和有序性,但是volatile无法保证原子性
内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前
读屏障:Load Barrier
在读指令之前插入读屏障,让工作内存或者CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新的数据
写屏障:Store Barrier
在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中
JVM中提供了四大内存屏障指令
loadload();
storestore();
loadstore();
storeload();
可以理解为: load:读 ; store:写
volatile写总结:
1.在每个volatile写操作的前面插入一个StoreStore屏障
2.在每个volatile写操作的后面插入一个StoreLoad屏障
volatile读总结:
1.在每个volatile读操作的后面插入一个LoadLoad屏障
2.在每个volatile读操作的后面插入一个LoadStore屏障