volatile:一种轻量级的同步机制
能够保证:1.内存可见性(通过强制刷新cpu中的数据) ;2.保证有序性;(通过禁止指令重排实现)
不能保证:** 原子性。
关于指令重排:指令重排是指在编译Java代码或者cpu执行字节码时,对字节码指令进行重新排序。
volatile 如何禁止指令重排?(通过cpu指令内存屏障)
-
1、在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
-
2、在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
关于内存屏障的补充说明:
-
1、LoadLoad屏障:
抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 -
2、StoreStore屏障:
抽象场景:Store1; StoreStore; Store2
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见 -
3、LoadStore屏障:
抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕。 -
4 、StoreLoad屏障:
抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
以下式关于内存可见性的代码测试:
public class TestVolatile{
volatile int num = 0;
public static void main(String[] args) {
TestAtomic tv = new TestVolatile();
new Thread() {
public void run() {
System.out.println("A com in");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
tv.num = 1;
System.out.println("num is changed,num="+tv.num);
};
}.start();
while(tv.num==0) {
//System.out.println(tv.num); 关于这句代码,加上会导致不能测试出加volatile和不加的区别。通过对字节码的
分析可知,这部分进行了两次从内存中取值,并不是一个原子操作。
}
System.out.println("main is finish");
}
}
关于原子性的代码测试:
public class TestVolatile {
volatile int num = 0;
public void add() {
num++;
}
public static void main(String[] args) {
TestVolatile tv = new TestVolatile();
for(int i = 0;i<20;i++) {
new Thread() {
public void run() {
for(int j = 0;j<1000;j++) {
tv.add();
}
}
}.start();
}
if(Thread.activeCount()>1) {
System.out.println(tv.num);
}
}
}
按照我们所想:20个线程,每个线程执行1000次++操作,最后的结果应该是20000,可是得到的结果却总是小于20000,实际上由于volatile并不能保证++操作的原子性,(关于i++操作的说明,分为三步,每个步骤中间能够被打断,1.内存到寄存器,2.寄存器自增,3.写回内存,关于赋值操作,其实际上是一个原子操作,只有写回内存这一步)。