Volatile关键字是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量, 相比synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。 但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
来看一段代码:
public class Test { public static void main(String[] args) { WangZai wangZai = new WangZai(); wangZai.start(); for(; ;){ if(wangZai.isFlag()){ System.out.println("hello"); } } } static class WangZai extends Thread { private boolean flag = false; public boolean isFlag(){ return flag; } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag = " + flag); } } }
你会发现,永远都不会输出hello这一段代码,按道理线程改了flag变量,主线程也能访问到的呀?
但是将flag变量用volatile修饰一下,就能输出hello这段代码
private volatile boolean flag = false;
每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且写会了,那其他已经读取的线程的变量副本就会失效了,需要对数据进行操作又要再次去主内存中读取了。
volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。
重排序需要遵守一定规则:
什么是重排序?
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
重排序的类型有哪些呢?
一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标,而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。
JMM对底层尽量减少约束,使其能够发挥自身优势。
因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。
一般重排序可以分为如下三种:
那 Volatile 是怎么保证不会被执行重排序的呢?
java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
是否能重排序第二个操作第一个操作普通读/写volatile读volatile写普通读/写NOvolatile读NONONOvolatile写NONO
举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
从上表我们可以看出:
需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
写
读
从JDK5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。
happens-before 关系的定义:
看到这儿,你是不是觉得,这个怎么和 as-if-serial 语义一样呢。没错, happens-before 关系本质上和 as-if-serial 语义是一回事。
as-if-serial 语义保证的是单线程内重排序之后的执行结果和程序代码本身应该出现的结果是一致的,
happens-before 关系保证的是正确同步的多线程程序的执行结果不会被重排序改变。
一句话来总结就是:如果操作 A happens-before 操作 B ,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。
在 Java 中,对于 happens-before 关系,有以下规定: