Volatile这个关键字在学习的时候并未太过注意,但是前段时间,在测试自己所用的一个小项目时,意外发现其等待时间太长,当时用的是synchronized关键字。后来使用Volatile关键字后,性能明显有所改善。
Volatile 在Java是可视化的意思,字面意思来说就是被该关键字所修饰的变量能被看见。听上去类似static,但是这个可视化是对于访问该变量的各个线程来讲的。
在了解该关键字前,我们需要先看一下线程的内存模型。
当我们执行操作时,会从内存中读取或者写入数据,但是与CPU的执行速度相比,其读取和写入速度要慢很多,如果直接从内存中读取,会造成CPU执行速度下降,性能浪费。所有我们会先将需要的数据存储到CPU的高速缓存中,在这里进行数据处理,当处理完毕后,再将高速缓存的内容写入内存中。
然而这种情况就会导致一种非常著名的问题——缓存一致性问题
如果我们有两个CPU去对一个共享变量进行操作。
num = 0;
num = num + 1;
此时,线程1从内存中读取到了num的初始值为0,然后将初始值读入缓存,然后CPU1进行处理。恰巧,线程2在此时也将num的初始值0读入了缓存,而这时,线程1还未将计算结果写入到内存中。那么就会发生该问题,二者高速缓存中的结果都是num=1,写入内存后与期望不同。
这样的原因在于对于单个CPU而言,都拥有着自己的缓存区域,对于多个CPU而言,没有volatile关键字修饰的遍历在各个CPU中是无法相互可见的,当其值已经发生改变后,其他CPU并不能立刻获取到该值,而是使用自己高速缓存内已经读取到的原始值。
而解决这个问题则有两种方式
- 加锁synchronize
- 使用Volatile关键字修饰
二者虽然都能解决该问题,但是其方式有着本质性区别。
synchronize锁的方式使将该部分代码块的锁权限交个其中一个线程,当该线程访问这段内容时,其他线程进行等待,等该线程访问完毕并将数据写入内存后解锁,由其他线程竞争锁权限。这个方法是最安全的方法,能从本质上消除多线程的安全问题。通过锁的方式,实际上将多线程处理转化为单线程轮流处理。但是其弊端就是低下的性能问题。如果有一个队列要访问某个值,且队列庞大,这样等待需要花费大量时间,阻塞情况非常严重。
而volatile关键字则是将变量可视化,在一个线程完成处理后,立刻将该值分享到其他线程进行数值更新,其他线程发现这个值已经改变,将从内存中重新读取。这样的好处十分明显,所有线程能真正实现同时工作,同时对一个变量进行操控,能够大大提高效率。但是其安全性没有synchronize高。
原子性
我们提到volatile就不得不提原子性的问题,简单来说,原子性的操作就是这些操作无法中断,只能一次完成。
我们用到的赋值和读取操作都是原子性操作。举个例子
{
int x = 1;//原子性操作,将值赋给了一个变量
int y = x;//非原子性操作,我们先要查找x值,之后再将值赋给y,两步操作
}
在多线程中,我们需要尽量保持操作为原子性操作。非原子性操作在售票厅机制中体现的很好,会出现同一时间多个线程对共享变量进行操作,虽然有了Volatile修饰,但是还是会出现负数。
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
这里有一段代码,按照我们理想情况,执行完成后其值应该为10000.但实际上,基本上都会小于10000.
我们用了volatile关键字修饰共享变量,为什么还会出现这种情况呢?
现在我们看下这种情况的发生。
首先我们要知道,自增操作并非原子性操作,它是通过取值和增加赋值完成的。假设线程1从主存中读取到了inc的值为10,下一步要进行增加的操作,但是此时被堵塞了。此时,线程2开始读取inc的值,按照我们的期望,应该读取到的是11,但是实际上,其读取到的是inc=10,因为线程1虽然进行了读取,但并未对值进行改变时,线程就堵塞了,此时,默认对值并没有改变,系统不会判断下一步是否要改变该值,所以其他线程不可见,依旧从缓存中读取,等线程2完成操作时,inc=11.而线程1从阻塞获取执行权,执行为完成的操作,进行增长,此时读取的还是高速缓存内的inc=10。这样就造成了两次操作,实际增加1.
所有,volatile并不能保证操作的原子性,当操作非原子性时,其并没有那么优秀。
适用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
下面列举几个Java中使用volatile的几个场景。
标记状态量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
其操作为原子性,直接赋值即可。