volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。
内存模型
程序在执行过程中,每条指令都是通过CPU来执行的,存在数据的读取和写入。程序运行过程中的临时数据存放在主内存(物理内存)中,cpu执行速度很快,要是直接从主内存中读取和写入数据,那执行指令的速度要慢很多,所以cpu要引入高速缓存也叫工作内存。
程序实际运行过程,会将运算需要的数据从主内存中复制一份到cpu高速缓存当中,直接从高速缓存中读取和写入数据,运算结束后再把高速缓存的数据刷新到主内存当中。
多线程共享变量
每个线程都有自己独立的工作内存,多个线程同时访问一个变量会存在缓存一致性问题。假如X的初始值为1,可能存在线程1和2都执行X=X+1,cpu执行时工作内存1和工作内存2中X的值都为1,两个线程执行完后刷新到主内存,最终导致执行结果为2,而不是3,。如下图:
如果线程1对变量的修改能够被线程二看到,需要做如下操作:
1.线程1把修改后的变量从工作内存1中刷新到主内存中
2.主内存把最新的变量值更新到线程2的工作内存中
并发编程
并发编程中有三个问题:原子性,可见性和有序性。
原子性
原子性:即一个操作或者多个操作要么全部执行且执行过程中不会被打断,要么就都不执行。
举例:从账户A向账户B转1000元,需要先从账户A减去1000元,再往账户B加上1000元。如果没有原子性保护,从账户A减去1000元,操作突然中止。导致A账户少了1000元,而B账户没有加上1000元。
可见性
可见性:指多个线程访问同一变量时,一个线程修改了变量值,其他线程能够立即看到修改的值。
举例:
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
int j = i;
线程1执行完后i的值为10,线程2执行完后j的值仍为0,这就是可见性问题。
有序性
有序性指程序执行的顺序按照代码的先后顺序执行。
举例:
int i = 0;
int j = 0;
i = 1; //语句1
j = 2; //语句2
程序在执行时不一定语句1先执行,语句2后执行,因为cpu对执行进行了重排序,因为处理器为了提高程序的运行效率,可能会对输入的代码进行优化,就不能保证执行的先后顺序和代码的先后顺序一样,但能保证执行的结果和代码顺序执行结果一致。
剖析volatile关键字
以上的讲述都是深入理解volatile关键字做铺垫,下边进入正题。
volatile关键字两层语义
一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备了两层语义:
1.保证了不同线程对变量进行操作时的可见性,即一个线程修改了变量的值,修改后的值对其他线程来说是立即可见的。
2.禁止进行指令重排序。
volatile不能保证原子性
如下代码示例:
public volatile int num = 0;
public void increase(){
num++;
}
public static void main(String[] args) throws InterruptedException {
VolatileTest volatileTest = new VolatileTest();
for (int i=0; i<10; i++){
new Thread(){
@Override
public void run(){
for (int j=0; j<1000; j++){
volatileTest.increase();
}
}
}.start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(volatileTest.num);
}
}
运行结果发现每次都不一样,我们期望的值是:10000,导致原因如下:
自增操作不是原子性的,而且volatile也无法保证对变量操作的原子性
如想保证原子性操作可以使用如下方法:
1.increase方法前加synchronized关键字。
2.increase方法中加锁。
3.使用AtomicInteger类。
volatile原理及实现机制
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
-
确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
-
它会强制将对缓存的修改操作立即写入主存。
-
如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile使用场景
synchronized关键字是防止多个线程同时执行一段代码,但会很影响执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1.对变量的写操作不依赖于当前值.
2.该变量没有包含在具有其他变量的不变式中.
我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字在并发时能够正确执行。