1.Volatile
volatile 是 Java虚拟机提供的一种轻量级的同步机制, 它不会引起线程上下文的切换和调度。 并且使用volitale关键字,可以保证可见性、不保证原子性、禁止指令重排序!
保证可见性
//线程1volitale boolean OK = false;while(!OK){doSomething();}//线程2OK = true;
一:使用volatile关键字会强制将修改的值立即写入主存。
二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量ok的缓存行无效。
三:由于线程1的工作内存中缓存变量ok的缓存行无效,所以线程1再次读取变量ok的值时会去主存读取。
不保证原子性
public class TestAtomicity {public volatile int variable = 0;//一个自增方法public void add() {variable++;}public static void main(String[] args) {final TestAtomicity testAtoc = new TestAtomicity();for(int i=0;i<20;i++){new Thread(){public void run() {for(int j=0;j<500;j++)testAtoc.add();};}.start();}while(Thread.activeCount()>1) //保证前面的线程都执行完Thread.yield();System.out.println(testAtoc.variable);//一个小于10000的数字}}
++操作的执行过程:
- 首先获取变量variable的值
- 将该变量的值+1
- 将该变量的值写回到对应的主内存中
1号线程刚刚要写1的时候被挂起,2号线程将1写入主内存,此时应该通知其他线程,主内存的值更改为1,由于线程操作极快,还没有通知到其他线程,刚才被挂起的线程1 将num=1 又再次写入了主内存,主内存的值被覆盖,出现了丢失写值。
可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
volitale保证原子性的三种方法:
- 加synchronized关键字
- 加lock锁
-
禁止指令重排序
重排序
编译器和处理器为了优化程序性能而对指令序列进行重排序,即编写的代码顺序和最终执行的指令顺序是不一致的。但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
//证明存在重排序 public class T10_InstructionRearrangement { public static int a = 0, b = 0, x = 0, y = 0; public static void main(String[] args) throws InterruptedException { int count = 0; while(true) { a = 0; b = 0; x = 0; y = 0; Thread thread1 = new Thread(() -> { a = 1; x = b; }); Thread thread2 = new Thread(() -> { b = 1; y = a; }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); // 如果没有指令重排序,可能的情况有 // x = 1 y = 1 // x = 0 y = 1 // x = 1 y = 0 // 当出现了x=0,y=0说明发生了指令重排 count ++; if (x == 0 && y == 0){ System.out.println("运行次数:" + count); System.out.printf("x = %d, y = %d\n", x, y); break; } } } } //结果会出现x = 0,y =0内存屏障
内存屏障 是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性。内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile读写语义
//x、y为非volatile变量 //flag为volatile变量 x = 2; //语句1 y = 0; //语句2 fag = true; //语句3 x = 4; //语句4 y = -1; //语句5flag变量为volatile变量,在进行指令重排序时,不会将语句3放到语句1、语句2前面,也不会将语句3放到语句4、语句5后面。 语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
volatile读写:
- 在每个volatile写操作的前面插入一个StoreStore屏障;
- 在每个volatile写操作的后面插入一个StoreLoad屏障;
- 在每个volatile读操作的后面插入一个LoadLoad屏障;
- 在每个volatile读操作的后面插入一个LoadStore屏障。
2.实现原理
字节码层面是ACC_VOLATILE标识。
JVM层面是内存屏障。
操作系统层面是通过lock前缀指令实现的。
lock指令的几个作用:
- 锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
- lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序
3.使用场景
DCL
public class DoubleCheckLock { private volatile static DoubleCheckLock instance; private DoubleCheckLock(){} public static DoubleCheckLock getInstance(){ //第一次检测 if (instance==null){ //同步 synchronized (DoubleCheckLock.class){ if (instance == null){ //多线程环境下可能会出现问题的地方,非原子操作 instance = new DoubleCheckLock(); } } } return instance; } }instance = new DoubleCheckLock(), 可分解为: 1.memory =allocate(); //分配对象的内存空间
2.ctorInstance(memory); //初始化对象
3.instance =memory; //设置instance指向刚分配的内存地址 有可能出现1,3,2的顺序 ,虽然instance不为空,但是对象也有可能没有正确初始化,会出错。
Java对象的创建不是原子性操作,所以有指令重排序的可能。为了禁止指令重排序,所以要引入 volatile
volatile和synchronized
- volitale仅能使用在变量级别,synchronized则可以使用在变量和方法上
- volitale仅能实现变量的修改可见性(缓存数据不对问题),原子性保证不了;而synchronzied则可以保证变量的修改可见性和原子性
- volatile不会造成线程阻塞,而Synchronized会造成线程阻塞
- voltaile本质是在告诉jvm当亲变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问这个变量,其他线程被阻塞

