volatile的特性
理解volatile特性的一个好方法就是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
package text1;
public class Threadtest implements Runnable{
int v1 = 0;
public synchronized void set(int l) {
v1 = l;
}
public synchronized int get() {
return v1;
}
public void getAndIncrement() {
int temp = get();
temp += 1L;
set(temp);
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 100000; i++)
getAndIncrement();
System.out.println(Thread.currentThread().getName()+"::"+v1);
}
public static void main(String[] args) throws InterruptedException {
duoxiancheng t1 = new duoxiancheng();
Thread t3 = new Thread(t1);
Thread t4 = new Thread(t1);
t3.start();
t4.start();
}
}
如上所示只要是volatile变量,对该变量的读/写就具有原子性。
简而言之:volatile变量自身具有以下特性
可见性:对一个volatile变量的读,总能看到(任意线程)对这个volatile变量最后的写入。(这个就类似于git操作,push之前必须pull一下更新到最新的版本。)
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile写-读建立的happens关系
volatile对线程的内存可见性的影响比volatile自身的特性更为重要。
从内存语义的角度来讲,volatile的写-读与锁的释放-获取有相同的内存效果:volatile写和锁的释放有相同的内存语义,volatile读与锁的获取有相同的内存语义。
假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before规则,这个过程建立的happens-before关系可以分为三类
1.根据程序次序规则,1hanppens-before2;3hanppens-before4.
2.根据volatile规则,2hanppens-before3.
3.根据hanppens-before的传递性规则1hanppens-before4。
package text1;
public class VolatileExample {
int a = 0;
volatile boolean flag =false;
public void writer() {
a=1; //1
flag=true; //2
}
public void reader() {
if(flag) { //3
int i =a; //4
}
}
}
volatile写-读的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。也就是说,比如上面那个程序为例,当一个线程执行writer()方法只后,a和flag的值都变了,而线程B存到本地内存的a和flag的值就失效了。
volatile内存语义的实现
重排序分为编译器重排序和 处理器重排序,前者时在编译时发生的重排序,后者时在程序执行时发生的重排序。
下面时基于保守策略的JMM内存屏障插入策略。
1.在每个volatile写操作的前面插入一个StoreStore屏障(不能同时写)
2.在每个volatile写操作的后面插入一个StoreLoad屏障(volatile写的时候不能读)
3.在每个volatile读操作的后面插入一个LoadLoad屏障(不能同时读)
4.在每个volatile读操作的后面插入一个LoadStore屏障(volatile读的时候不能写)
package text1;
public class VolatileBarrierExample {
int a;
volatile int v1=1;
volatile int v2=2;
void readAndwrite() {
int i=v1; //第一个volatile读
int j=v2; //第二个volatile读
a=i+j; //普通写
v1=i+1; //第一个volatile写
v2=j+2; //第二个volatile写
}
}
根据上面的图给出总结
volatile读或者写后面时哪种屏障根据本身和后面的操作来定义,比如第一个volatile读后面时volatile读,那么第一个volatile读操作的后面就是LoadLoad屏障(不能同时读)
注意:最后的那个StoreLoad屏障不能省略,编译器无法准确的判断后面是否会有volatile读或者写,为了安全起见,编译器通常会在这里插入一个StoreLoad屏障。