原理
volatile 关键字为域变量的访问提供了一种免锁机制
, 使用 volatile 修饰域相当于告诉虚拟机该域可能会被其他线程更新
, 因此每次使用该域就要重新计算
, 而不是使用寄存器中的值
。
需要注意的是, volatile 不会提供任何原子操作
, 不能保证事务的A,
它也不能用来修饰 final 类型的变量
在 JVM 底层volatile 是采用“内存屏障”
来实现的。 观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码
发现, 加入volatile 关键字时, 会多出一个 lock 前缀指令
, lock 前缀指令实际上相当于一个内存屏障, 内存屏障会提供 3 个功能:
它确保 指令 重排序时 不会把其后面的指令 排到内存屏障之前的位置, 也不会把 前面的指令排到内存屏障的后面;
即在 执行到内存屏障这句指令 时, 在它前面的操作已经全部完成;
它会强制将 对缓存的修改 操作 立即写入主存;
不能保证原子性
一个比较典型的例子是++运算符
。
在下面的代码中,一共创建了1000个线程,预期应该是加了1000次,那么number的值应该是1000,实际上有可能并不是。
这是因为,++运算符并不是一次操作
。以number++为例,可以看作是,先从主内存中取出number的值,刷新工作内存,然后将其加1,刷新主内存
,这么几个步骤。
而volatile并不能保证原子性,这就意味着,有可能出现这种情况:
1)线程A
获取到主内存的number的值(假设为10)到工作内存
2)此时CPU调度
,A暂停
,线程B开始执行,同样从主内存中获取到number为10,number++后,number为11,刷新到主内存
3)线程A继续执行number++
,它的工作内存中number为10,执行完毕刷新到主内存,此时,number的值为11
也就是说,AB两个线程同时进行了+1操作,但最终的结果,只加了1
volatile适用场景
1)对共享变量的写操作,不依赖于其之前的值
不合适:number++, number = number * 2, number += 1等
合适:boolean值
2)该变量没有包含在具有其他变量的不变式中,也就是说,不同的volatile变量之间,不能互相依赖
AtomicInteger实现递增
上面我们已经知道一个整型的共享变量要实现递增,如果使用++运算符,即使加上volatile关键字,也是无法保证其原子性的
。而如果在访问变量时加上synchronized块,或者可重入锁,开销又太大
。
JDK1.5之后,可以使用AtomicInteger
进行递增。该类是线程安全的。
将上面的代码修改如下,就可以保证原子性和可见性。
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileDemo {
private AtomicInteger number = new AtomicInteger(0);
public void increase() {
number.incrementAndGet();
}
public int getNumber() {
return number.intValue();
}
public static void main(String[] args) {
final VolatileDemo demo = new VolatileDemo();
for (int i = 0; i <= 999; i++) {
new Thread(new Runnable() {
@Override
public void run() {
demo.increase();
}
}).start();
}
//线程未执行完,主线程让出CPU资源
while(Thread.activeCount() > 1){
Thread.yield();
}
//待上面的线程都执行完了,再打印,避免打印的不是最后的数据
System.out.println(demo.getNumber());
}
}