volatile和final在线程同步时起到很大的作用,那么在Java内存中这两个关键字是如何和线程同步关联起来呢,以及线程的happen-before规则又是怎么定义的呢?
volatile用来修饰变量,可以保证变量每次都是从主内存中读取,但是它不保证原子性,先来看看具体的例子:
public class VolatileExample {
private static volatile long v1 = 10L;
public static void setV1(long l){
v1 = l;
}
public static long getV1(){
return v1;
}
public static void getAndIncrement() {
v1++;
}
public static void main(String[] args){
for (int i = 0;i < 1000;i++){
new Thread(new Runnable() {
@Override
public void run() {
getAndIncrement();
}
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: "+v1);
}
}
上面getAndIncrement()等同于下面这个:
/**
* 这里有三个操作,读取V1的值,对V1进行+1操作,写V1的值,如果线程1进行到第二步时,
* 此时线程2去读取V1的值,还是V1没有+1前的值,所以此时取的值就不对了,因为线程1还没
* 有来得及将V1的值刷新到主内存,所以说明volatile能够保证每次的操作能够被其他线程及时
* 由于对操作不具有原子性,当一个操作分步执行时,其他线程就有可能取到脏数据。
* */
public static void getAndIncrement() {
v1++;
}
看下下面的例子:
class VolatileExample{
int a = 0;
volatile boolean flag = false; //volatile修饰变量
public void writer(){
a = 1; // 语句1
flag = true; // 语句2
}
public void reader(){
if(flag){ // 语句3
int i = a; // 语句4
...
}
存在问题
若flag没有被volatile修饰,那么上面的语句1,2,3,4就存在重排序的可能,那么假设线程1执行writer()函数,线程2执行reader(),若线程1先执行了语句2,此时线程2执行语句3,发现flag 为true,那么就执行语句4,此时线程1还没有执行语句1,那么语句4获得的a值就不是1了。虚拟机的重排序只是保证同一个线程内的语句不会被重排序影响,但是线程间的执行顺序并不能保证,如果对flag使用了volatile修饰,就能避免上述问题,volatile对于重排序有如下规则:
volatile重排序规则:
1.当第二个操作是volatile写操作时,不管第一个操作是什么,禁止重排序,所以语句1和2不能重排序
2.当第一个操作是volatile读操作时,不管第二个操作是什么,禁止重排序,所以语句3和4不能重排序
3.当第一个操作是volatile写,第二个操作是volatile读时,禁止重排序
所以,有了volatile修饰,上述语句1和2不能进行重排序,语句3和4也不会重排序,那么在两个线程中执行上述reader()和writer()就不会出现上述问题。
final对于重排序规则有如下的限制:
final域的写排序不能被重排序到构造函数之外,即是实例对象在被任意线程可见之前,它的final域一定已经被初始化过了,但是若普通域就不具有这个保证,Java内存模型会在构造函数返回之前,加入一个StoreStore屏障,即:
写final域;
StoreStore屏障;
构造函数return
看看以下代码:
public class FinalExample{
static FinalExample obj;
int i; //普通变量
final int j ; //final变量
public FinalExample(){
i = 10; //构造函数返回和对i写值这两个操作可能重排序
j = 20; //构造函数返回之前保证j已经写入20
}
public void writer(){
/**
* 在执行构造函数时,会保障j在构造函数return前初始化,但是不保障普通域i在构造函数返回
* 前初始化
* */
obj = new FinalExample();
}
public void reader(){
FinalExample object = obj;
/**
* 若线程B执行这个reader函数,由于重排序,可能obj已经初始化到实例了,但是i = 10还没
* 有执行,所以获取到的i值是原始值。
* */
int a = obj.i;
/**
* 但由于final域的写后面加入了内存屏障StoreStore,禁止处理器把final域的写重排序到构造
* 函数之外,所以这里获取到的j值肯定是正确的。
* */
int b = obj.j;
}
在一个线程中,初次读对象引用与初次读对象包含的final域,Java 内存模型禁止这两个操作重排序,编译器会在读final域前插入一个LoadLoad屏障,即:
读对象引用 ;
LoadLoad屏障;
读final域;
例子如下:
public class FinalExample {
private final int i;
private static FinalExample obj;
public FinalExample(){
i = 2; //语句1
obj = this; //语句2,这两个语句可能重排序,而且这个语句造成了对象引用溢出
}
public static void writer(){
new FinalExample(); //线程A执行writer()
}
public static void reader(){
if(obj != null){ //线程B执行reader(),若语句2先执行,此时obj不为空,但是事实上i还没有初始化,造成final域在构造函数溢出。
int temp = obj.i;
}
}
}
happen-before指定两个操作之间的执行顺序,这两个操作可以是一个线程之内,也可以是不同线程之间,JMM(Java内存模型)通过happen-before规则向程序猿提供跨线程的内存可见性,即如果线程A的写操作a happen-before于线程B的读操作,尽管a操作和b操作在不同的线程中执行,但是JMM向程序猿保证a的结果对b操作可见。
1.程序顺序执行规则: 一个线程中的任意操作,happen-before于该线程的任意后续操作
2.监视器锁规则: 对一个锁的解锁,happen-before于后续对该锁的加锁
3.volatile规则: 对一个volatile变量的写操作,happen-before于后续对该volatile的读操作
4.传递性: 如果A happen-before于 B,B happen-before C,那么A happen-before C
5.start()规则 如果线程A执行ThreadB.start()启动线程B,那么A的ThreadB.start()操作happen-before于线程B的任意操作
6.join()规则 如果线程A执行ThreadB.join()并成功返回,那么线程B的任意操作happen-before于线程A的ThreadB.join().
总结
总结下来,其实happen-before就是JMM对于程序员的一个保证,以及对于编译器和处理器执行重排序时的约束。