volatile关键字详解
简介
volatile 是Java中的一个关键字,它用于修饰变量,是Java提供的轻量级同步机制,在并发编程中扮演重要角色。相较于重量级锁(如synchronized),volatile更为轻量,避免了庞大的开销。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

适用场景:volatile 适用于对变量的读取和写入操作都是独立的、不依赖于其他变量的情况。常见的使用场景包括标志位的设置和检查、线程的启动和停止等。
特点
-
可见性(Visibility):当一个变量被声明为 volatile 时,它保证了对该变量的读取和写入操作都将直接在
主内存中进行,而不会使用线程的本地缓存(工作内存)。这意味着一个线程对 volatile 变量的修改对其他线程是可见的,即使其他线程使用了本地缓存,也能立即看到修改后的值。 -
禁止指令重排序(Ordering):volatile 变量的读写操作具有禁止指令重排序的特性。这意味着在 volatile 变量的写操作之前的代码不会被重排序到写操作之后,保证了代码执行的有序性。这对于实现线程安全的双重检验锁方式等场景非常重要。
-
不保证原子性(Atomicity):虽然 volatile 提供了可见性和有序性,但它不保证复合操作的原子性。如果一个操作包含多个步骤,每个步骤都需要保证原子性,那么仍然需要使用其他线程同步机制,如锁或原子类。
示例
双重校验锁实现对象单例(线程安全)
public class Singleton {
// 使用 volatile 关键字确保多线程环境下的可见性
private static volatile Singleton instance;
// 私有构造方法,防止外部实例化
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,避免不必要的同步
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,确保只创建一个实例
instance = new Singleton();
}
}
}
return instance;
}
}
双重检验锁方式实现单例模式的原理:
-
我们将 instance 声明为 volatile 类型的变量。这是为了确保多线程环境下对 instance 的可见性,以防止出现线程安全问题。
-
在 getInstance 方法中,首先检查 instance 是否为 null。如果 instance 不为 null,表示已经创建了单例对象,直接返回它,无需进一步的同步操作。
-
如果 instance 为 null,进入同步块(synchronized block)。在同步块中,再次检查 instance 是否为 null,这是为了避免多个线程同时通过第一次检查,只有一个线程能够进入同步块。
-
如果 instance 仍然为 null,则在同步块内创建一个新的 Singleton 实例,并将其赋值给 instance 变量。
这种双重检验锁方式实现单例模式的好处是,在大多数情况下,多个线程不需要进入同步块,因此提高了性能。只有在第一次创建单例对象时,才需要进行同步,保证了线程安全。这种方式被称为“双重检验锁”是因为它在两次检查 instance 变量是否为 null 后才创建实例。这是一种常见的用于实现懒汉式单例模式的方式。
volatile 如何禁止指令重排序?
instance = new Singleton(); 这段代码其实是分为三步执行:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance() 后发现 instance 不为空,因此返回 instance,但此时 instance 还未被初始化。
volatile原子性测试
public class VolatileAtomicityDemo {
public volatile static int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo();
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 可以保证变量的可见性嘛!
也就是说,如果 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都可以。
使用 synchronized 改进:
public synchronized void increase() {
inc++;
}
使用 AtomicInteger 改进:
public AtomicInteger inc = new AtomicInteger();
public void increase() {
inc.getAndIncrement();
}
使用 ReentrantLock 改进:
Lock lock = new ReentrantLock();
public void increase() {
lock.lock();
try {
inc++;
} finally {
lock.unlock();
}
}
volatile可见性测试
public class VolatileExample {
private volatile boolean flag = false;
public void toggleFlag() {
flag = !flag;
}
public boolean isFlag() {
return flag;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread writerThread = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
example.toggleFlag();
System.out.println("Flag is set to true.");
});
Thread readerThread = new Thread(() -> {
while (!example.isFlag()) {
// 等待flag变为true
}
System.out.println("Flag is true, exiting.");
});
writerThread.start();
readerThread.start();
}
}
在上述示例中,flag 变量被声明为 volatile,这确保了在 writerThread 修改 flag 时,readerThread 能够立即看到 flag 的修改,从而退出循环。这演示了 volatile 的可见性特性。

浙公网安备 33010602011771号