volatile概述
volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色。同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,系统开销比较小。如果一个变量用volatile修饰了,则所有线程看到这个变量的值是一致的,如果某个线程对这个变量进行了更新,则其他的线程可以立马看到这个更新,这就是线程可见性。
在其他的情况下,系统通常会对我们的对象进行缓存,当线程想要获取某个变量时,会先向缓存查找。如果某个变量已经更新了,但是在缓存中仍然是之前未改变的值,这样其他的线程访问这个变量的值时,得到的就不是最新的数据了。引入volatile,告知系统,这个值一旦被更新,相对应在缓存中的数据也更新,而不是等待某个时间节点再去更新。通过volatile修饰的数据可以实现线程安全。
volatile的特性
原子性
先解释下原子性是什么。原子性其实就是一个操作或多个操作,要么全部执行且执行的时候不被打断,要么就不执行。这个在数据库中的事务中有很好的体现,事务中如果某一步执行失败,则进行回滚,将已经执行过的操作撤回。
看看下面这个栗子中哪些是原子性的
i=0;//1
j=i;//2
i++; //3
i=j+1; //4
在java中,对基本数据类型的变量定义和赋值是原子操作。因此 1是原子性的;2其实经历了两步,读取i,将i赋值给j;3经历了三步操作,读取i,i+1,将i+1赋给i;4同3。
在单线程中,我们可以认为上面的操作都是原子性的,但是在多线程下,就只有1符合原子性。volatile无法保证复合操作的原子性,只有锁和synchronized可以保证复合操作的原子性。
举个栗子:
public class VolatileTest {
private static volatile int initvalue=1;
private final static int MAX=10;
public static void main(String[] args) {
Thread reader = new Thread() {
@Override
public void run() {
while(initvalue<=MAX) {
System.out.println(Thread.currentThread().getName()+" :"+initvalue++);
}
}
};
Thread updater = new Thread() {
@Override
public void run() {
while(initvalue<=MAX) {
System.out.println(Thread.currentThread().getName()+" :"+initvalue++);
}
}
};
reader.start();
updater.start();
}
}
打印结果
Thread-0 :1
Thread-1 :2
Thread-0 :3
Thread-1 :4
Thread-1 :6
Thread-0 :5
Thread-1 :7
Thread-0 :8
Thread-1 :9
Thread-0 :10
我们可以发现并不是按1-10的顺序输出的。
initvalue++这已操作可分为三步:
- 因为initvalue是被volatile修饰的,所以从主存中读取initvalue->假设此时为4
- initvalue=4+1;
- 将initvalue读入主存
因为volatile不满足原子性,线程可能执行到一半就失去了控制权,可能当线程A正在执行initvalue=4+1,还没有读入主存时,线程B又从主存读取了initvalue,这样就会发生不符合逻辑的错误。
可见性
volatile可以实现线程可见性
有序性
在java内存模型中,为了效率,允许编译器和处理器对执行进行重排序。重排序不会影响单线程的执行结果,但是多线程会受影响。volatile可以保证有序性。volatile可以禁止指令重排序若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
volatile禁止重排序规则:
- 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
- .当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序
volatile的内存语义及其实现
在JVM中,java之间的通信时通过共享内存实现的。volatile的内存语义是:
- 当写一个volatile变量时,会将该线程对于的本地内存中的共享变量立即刷新到主内存中
- 当读一个volatile变量时,设置当前线程下的本地内存中的共享变量无效,直接从主内存中读取变量
volatile的底层实现是通过插入内存屏障的:
- 在每一个volatile写操作前面插入一个StoreStore屏障
- 在每一个volatile写操作后面插入一个StoreLoad屏障
- 在每一个volatile读操作后面插入一个LoadLoad屏障
- 在每一个volatile读操作后面插入一个LoadStore屏障
volatile与happens-before
看个栗子
public class Test{
int i=0;
volatile boolean flag=false;
public void write(){ //Thread A
i=1; //1
flag=true; //2
}
public void read(){ //Thread B
if(flag){ //3
system.out.println("i:"+i); //4
}
}
}
回顾上面的重排序规则:
- 当第二个操作是voaltile写时,无论第一个操作是什么,都不能进行重排序
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能进行重排序
- .当第一个操作是volatile写时,第二个操作是volatile读时,不能进行重排序
通过这个栗子结合volatile的重排序规则,我们将这个重排序规则讲得更简单点:
- volatile读之后不能和普通写和普通读重排序
- volatile写之前不允许和普通写重排序,volatile写之后不允许和普通写/读重排序。
依据happens-before原则
- 按照happens-before程序顺序原则:1 happens before 2;3 happens before 4
- 按照happens-beforevolatile原则:2 happens-before 3
- 按照传递性原则:1 happends before 4
因为 1 happens before 4 那么1对4是可见的。因此ThreadB执行的read()打印出的值就是ThreadA中的值
Java内存模型以及CPU缓存不一致
我们大家都知道,CPU的执行速度比内存的读取速度快,因此引入Cache缓存来改善这一问题。将一部分数据从主存写入Cache。CPU先从Cache中读取数据,如果读取不到再从主存中读取,然后将数据放入Cache,这就是JMM的大致模型。但是由于引入Cache,从而引入了另一个问题。在多CPU多线程下,会将数据缓存到各自CPU对应的位置,也就是各自的虚拟机栈,读数据时也从自己的虚拟机栈读取,这样导致不同线程间数据不同步。再严谨一点说,如果JVM发现我们的某个线程操作只有读,没有任何写操作,JVM就会使这个线程一直读取缓存的数据,而不更新主存到缓存的数据;如果线程中有写的操作,就会发生缓存数据的更新。
看个栗子:
public class VolatileTest {
private static int initvalue=0;
private final static int MAX=5;
public static void main(String[] args) {
Thread reader = new Thread() {
@Override
public void run() {
int localvalue=initvalue;
while(initvalue<MAX) {
if(localvalue!=initvalue) {
System.out.println(Thread.currentThread().getName()+" :"+initvalue);
localvalue=initvalue;
}
}
}
};
Thread updater = new Thread() {
@Override
public void run() {
while(initvalue<MAX) {
try {
initvalue++;
System.out.println(Thread.currentThread().getName()+" :"+initvalue);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
reader.start();
updater.start();
}
打印结果:
Thread-1 :1
Thread-1 :2
Thread-1 :3
Thread-1 :4
Thread-1 :5
我们可以发现在线程0中initvalue的值一直恒等于localvalue。由此证明:在线程只有读操作的情况下,JVM不会更新主存数据到缓存上。
解决Cache缓存一致性方案,
- 给数据总线枷锁-串行读
- CPU高速缓存一致性协议
一致性协议的核心思想(volatile):当CPU写入数据时,如果发现该变量被共享(其他CPU也存在该变量的副本),会发出一个信号,通知其他CPU该变量的缓存无效了,其他CPU要读取该变量就只能去主存读取了。