1. 为什么需要volatile关键字
1.1 所谓多线程变量“不可见”问题
volatile关键字修饰的变量可以在多个线程之间保持“可见性”。这样的解释有些抽象,我们来看一个例子。
假设下面这种场景:我们来模拟一个网站访问计数器。定义一个NetAccessor类,类中有变量counter用来记录网站访问总数,默认counter等于0。
public class NetAccessor {
public static int counter = 0;
public static void access(){
counter = counter +1 ;
}
}
假设有两个客户访问了这个网站,即有两个线程A和B,A会调用access方法给counter加1,然后B也会调用access给counter加1,此时按我们的设想,网站的总访问量应该为2。代码如下所示,运行一下看看结果是多少。
public class NetAccessor {
public static int counter = 0;
public static void access(){
counter = counter + 1;
}
public static void main(String[] args) {
Thread A = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
access();
}
});
Thread B = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
access();
}
});
A.start();
B.start();
System.out.println("Now the total access count is : "+counter);
}
}
多次运行代码,有时我们会看到正确的结果
Now the total access count is : 2
有时,我们会看到下面这种结果
Now the total access count is : 1
也就是说,多个线程间共享的变量在被A线程修改后,可能B线程并不知道该变量已经被修改了。也就是存在变量在线程间“不可见”的问题。这是为什么呢?
原因是:在JVM中,为了提高各个线程的执行效率,每个线程都会从主线程的内存中复制自己所需要的变量到自己的CPU Cache中,这样对这些变量进行操作时,不用来回切换到主线程内存,可以提高执行效率(见下图)。比如上例,在线程A启动时,它会从主线程中复制变量counter到自己的内存空间,此时counter是0,然后线程B启动时,也会从主线程复制变量counter,此时counter还是0,而当线程A操作counter+1将counter变为1时,B线程对线程A中的counter是不可见的,还是0,所以线程B再执行counter+1,counter的值还是1。此时两个线程无论谁将counter的值flush到主线程中,counter都只会是1而不会是2(什么情况会是2呢?恰巧线程B在读取counter之前,线程A已经完成了操作counter+1,并flush到了主线程上,这时B拿 到的counter变量值就是1了。有点儿绕,简而言之,就是多个线程间的共享变量存在“不可见”问题。)
1.2 解决“不可见”问题的方法
- 使用synchronized关键字,对上例中操作共享变量counter进行加锁,保证同时只有一个线程操作该变量。但这种方式性能较低,如果有成千上万个线程操作同一变量,则只能有一个得到锁,其它的都得阻塞等待。
- 使用volatile关键字,让共享变量counter在多个线程间可见。子线程在读取counter的值前,会直接从主线程内存中读取,在写入counter的值时,会直接写到主线程内存中。这样牺牲了一定的并发性能,但保证了该共享变量在多线程间可见(其实就是用的都是主线程内存中的变量,而不是复制到各线程CPU Cache中的变量了)。
改造上例中代码,只需加个一个volatile进行修饰共享变量即可
public class NetAccessor {
public static volatile int counter = 0;
public static void access(){
counter = counter +1 ;
}
}
2.volatile关键字在jdk5.0之后新增的特性
在JDK5.0之后,volatile关键字的意义不只是在于“从主线程内存中直接读写变量”那么简单了。还涉及以下两个新的规则:
- 如果线程A对某个volatile变量执行写操作,然后线程B对同一变量执行读操作,那么所有A线程中在执行写操作前的变量,也会随着该volatile变量flush到主线程中,对线程B保持可见性。
- 读写的顺序是非常重要的。线程A对某个volatile变量执行写操作之后的变量,对B线程还是不可见的。
运用这种特性,开发人员可以轻松实现以下交换逻辑,只要注意变量修改的顺序,无需再对每个要可见的变量都加上volatile关键字:
public class Exchanger {
private Object object = null;
private volatile hasNewObject = false;
public void put(Object newObject) {
while(hasNewObject) {
//wait - do not overwrite existing new object
}
object = newObject;
hasNewObject = true; //volatile write
}
public Object take(){
while(!hasNewObject){ //volatile read
//wait - don't take old object (or null)
}
Object obj = object;
hasNewObject = false; //volatile write
return obj;
}
}
3.volatile关键字不能保证原子性
volatile虽然可以让多个线程之间实现共享变量的可见性,但却不能保证原子性。
比如:我们有100个线程,共享同个volatile变量counter(初始为0),这100个线程执行同样的操作,即在counter基础上累加1000,按照设想,如果volatile变量具有原子性(同时只有一个线程访问),则最终的值应该有100000。那么结果如何呢?请看示例代码:
public class VolatileNoAtomic extends Thread{
private static volatile int count;
private static void addCount(){
for (int i = 0; i < 1000; i++) {
count++ ;
}
System.out.println(count);
}
public void run(){
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
}
结果可以看到:大多数情况下并不是我们期望的输出结果100000。这是因为volatile修饰的变量并不具备原子性,多个线程可能同时读取到同个volatile变量的值 。
4.解决原子性问题
JDK提供了Atomic系列类用于对基本数据类型(如int, long等)实现原子操作,使用同一时间只能有一个线程操作该变量。对上面示例进行修改如下:
public class VolatileNoAtomic extends Thread{
private static AtomicInteger count = new AtomicInteger(0);
private static void addCount(){
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
System.out.println(count);
}
public void run(){
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
}
这样便可以解决原子性问题,但是要注意的是,Atomic对象的原子性只针对单一次原子操作,并不能保证多个操作的原子性,此时应该考虑使用锁了。
注意
以上内容源自互联网相关资料及本人学习与工作经验,仅为学习及技术分享所用,切勿用于商业用途,转载请注明出处。