前言
在 Java 中,volatile 关键字可以保证变量的可见性,如果将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
一、volatile 关键字
volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取
JMM(Java 内存模型)强制在主存中进行读取
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
二、禁止指令重排序
在 Java 中,volatile 关键字除了可以保证变量的可见性,还有一个重要的作用就是防止 JVM 的指令重排序。 如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:
public native void loadFence();
public native void storeFence();
public native void fullFence();
理论上来说,这个三个方法也可以实现和volatile禁止重排序一样的效果,只是会麻烦一些。
二、双重检验锁方式实现单例模式
双重校验锁实现对象单例(线程安全):
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。
例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
三、volatile 不能保证原子性
volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。
例如:
public class VolatileAtomicityDemo {
public volatile static int inc = 0;
//对volatile变量inc执行+1操作
public void increase() {
inc++;
}
public static void main(String[] args) throws InterruptedException {
//创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5);
VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo();
//使用每个线程调用increase方法去执行500次+1操作
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 500; j++) {
volatileAtomicityDemo.increase();
}
});
}
// 等待1.5秒,保证上面程序执行完成
Thread.sleep(1500);
System.out.println(inc);
threadPool.shutdown();
}
}
正常情况下,运行上面的代码理应输出 2500。但真正运行了上面的代码之后,会发现每次输出结果都小于 2500。
也就是说,如果 volatile 能保证 inc++ 操作的原子性的话。每个线程中对 inc 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是 5*500=2500。
很多人会误认为自增操作 inc++ 是原子性的,实际上,inc++ 其实是一个复合操作,包括三步:
- 读取 inc 的值。
- 对 inc 加 1。
- 将 inc 的值写回内存。
volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:
- 线程 1 对 inc 进行读取操作之后,还未对其进行修改。线程 2 又读取了 inc的值并对其进行修改(+1),再将inc的值写回内存。
- 线程 2 操作完毕后,线程 1 对 inc的值进行修改(+1),再将inc 的值写回内存。
这也就导致两个线程分别对 inc 进行了一次自增操作后,inc 实际上只增加了 1。
其实,如果想要保证上面的代码运行正确也非常简单,
利用 synchronized、Lock或者AtomicInteger都可以。
public synchronized void increase() {
inc++;
}
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally {
lock.unlock();
}
}