一、volatile介绍
volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,相比使用synchronized所带来的庞大开销,倘若能恰当的合理的使用volatile,自然是美事一桩。
为了能比较清晰彻底的理解volatile,我们一步一步来分析。首先来看看如下代码
public class volatileDemo1 {
static boolean flag = true;
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t -----come in");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
},"t1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t -----come in");
while (flag) {
}
System.out.println(Thread.currentThread().getName()+"\t -----flag被设置为false,程序停止");
},"t2").start();
}
}
上面这个例子,模拟在多线程环境里,t1线程对flag共享变量修改的值能否被t2可见,即是否输出 “-----flag被设置为false,程序停止” 这句话?
这个结论会让人有些疑惑,可以理解。因为倘若在单线程模型里,因为先行发生原则之happens-before,自然是可以正确保证输出的;但是在多线程模型中,是没法做这种保证的。因为对于共享变量flag来说,线程t1的修改,对于线程t2来讲,是"不可见"的。也就是说,线程t2此时可能无法观测到flage已被修改为false。那么什么是可见性呢?
所谓可见性,是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中。很显然,上述的例子中是没有办法做到内存可见性的。
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新共享变量。
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取,从而保证了可见性。
volatile变量有2大特点,分别是:
- 可见性
- 有序性:禁重排!
volatile不保证:
- volatile关键字 不具备【互斥性】
- volatile关键字 不能保证变量的【原子性】
重排序是指编译器和处理器为了优化程序性能面对指令序列进行重新排序的一种手段,有时候会改变程序予以的先后顺序。
- 不存在数据以来关系,可以重排序;
- 存在数据依赖关系,禁止重排序。
- 但重排后的指令绝对不能改变原有串行语义!
那么volatile凭什么可以保证可见性和有序性呢?? ----内存屏障Memory Barrier~
二、内存屏障
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
Java中的内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性。
- 内存屏障之前 的所有 写操作 都要 回写到主内存,
- 内存屏障之后 的所有 读操作 都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
粗分主要是以下两种屏障:
- 读屏障(Load Memory Barrier) :在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据。
- 写屏障(Store Memory Barrier) :在写指令之后插入写屏障,强制把写缓冲区的数据刷回到主内存中。