📖目录
- 引言
- [1. 什么是volatile?------ 让数据"看得见"的魔法](#1. 什么是volatile?—— 让数据"看得见"的魔法)
- [2. 内存可见性:为什么需要volatile?](#2. 内存可见性:为什么需要volatile?)
-
- [2.1 代码示例1:内存可见性问题演示](#2.1 代码示例1:内存可见性问题演示)
- [2.2 代码示例2:使用volatile解决内存可见性问题](#2.2 代码示例2:使用volatile解决内存可见性问题)
- [3. 指令重排序:为什么需要volatile?](#3. 指令重排序:为什么需要volatile?)
-
- [3.1 指令重排序可能会导致问题](#3.1 指令重排序可能会导致问题)
- [3.2 代码示例3:指令重排序问题演示](#3.2 代码示例3:指令重排序问题演示)
- [3.3 代码示例4:使用volatile解决指令重排序问题](#3.3 代码示例4:使用volatile解决指令重排序问题)
- [4. volatile与synchronized的对比](#4. volatile与synchronized的对比)
-
- [4.1 二者的对比](#4.1 二者的对比)
- [4.2 代码示例5:volatile与synchronized的对比](#4.2 代码示例5:volatile与synchronized的对比)
- [5. volatile的高级用法](#5. volatile的高级用法)
-
- [5.1 代码示例6:双重检查锁定(DCL)模式](#5.1 代码示例6:双重检查锁定(DCL)模式)
- [5.2 代码示例7:volatile与原子类的组合使用](#5.2 代码示例7:volatile与原子类的组合使用)
- [6. volatile的适用场景和局限性](#6. volatile的适用场景和局限性)
-
- [6.1 适用场景](#6.1 适用场景)
- [6.2 局限性](#6.2 局限性)
- [7. 总结](#7. 总结)
- [8. 往期回顾](#8. 往期回顾)
- [9. 经典书籍推荐](#9. 经典书籍推荐)
- [10. 下一篇文章预告](#10. 下一篇文章预告)
- [11. 附录:volatile的底层原理](#11. 附录:volatile的底层原理)
引言
在之前的系列文章中,我们已经深入探讨了线程安全的各种工具和机制:
- 从
ArrayList的并发翻车说起,了解了主流线程安全集合 - 深度拆解了
ConcurrentHashMap、ThreadLocal、ReentrantLock - 探索了原子类、Disruptor、CountDownLatch、CyclicBarrier、Phaser、Condition
- 了解了CompletableFuture和信号量
在掌握了Exchanger的高级用法后,我们将在本篇文章中深入探讨volatile关键字。我们将从内存可见性、指令重排序等角度,通过生动的示例,揭示volatile如何在多线程环境中确保数据的一致性。同时,我们还会分析volatile与synchronized的区别,以及在实际开发中如何正确使用volatile。
1. 什么是volatile?------ 让数据"看得见"的魔法
在Java中,volatile关键字是一个轻量级的同步机制,它保证了变量的内存可见性 和禁止指令重排序,但不保证原子性。
让我们用一个生活化的例子来理解:
想象一下,你和家人共用一个冰箱。你妈妈在冰箱里放了一盒牛奶,然后去厨房做饭。你看到冰箱门关着,以为里面没有牛奶,就去买了新的。但其实冰箱里已经有牛奶了,只是你没有"看见"。
在多线程环境中,每个线程都有自己的工作内存(类似于你的"眼睛"),而主内存(类似于冰箱)中的数据变化可能不会立即被其他线程"看见"。
volatile关键字就像给冰箱加了一个"提醒":当冰箱里的牛奶被放进去或取出来时,会立即通知所有家人,确保每个人看到的牛奶状态是一致的。
2. 内存可见性:为什么需要volatile?
在Java中,为了提高性能,编译器和处理器会对代码进行优化,包括指令重排序和缓存机制。这意味着,一个线程对变量的修改可能不会立即被其他线程看到。
2.1 代码示例1:内存可见性问题演示
java
public class VisibilityExample {
private boolean flag = false;
public void writer() {
flag = true;
System.out.println("Flag set to true");
}
public void reader() {
while (!flag) {
// 无限等待
}
System.out.println("Flag is now true");
}
public static void main(String[] args) {
VisibilityExample example = new VisibilityExample();
Thread writerThread = new Thread(() -> example.writer());
Thread readerThread = new Thread(() -> example.reader());
writerThread.start();
readerThread.start();
}
}
执行结果
Flag is now true
Flag set to true
执行结果说明 :readerThread可能永远不会打印"Flag is now true",因为flag的更新对readerThread不可见。
2.2 代码示例2:使用volatile解决内存可见性问题
java
public class VisibilityExampleWithVolatile {
private volatile boolean flag = false;
public void writer() {
flag = true;
System.out.println("Flag set to true");
}
public void reader() {
while (!flag) {
// 等待flag变为true
}
System.out.println("Flag is now true");
}
public static void main(String[] args) {
VisibilityExampleWithVolatile example = new VisibilityExampleWithVolatile();
Thread writerThread = new Thread(() -> example.writer());
Thread readerThread = new Thread(() -> example.reader());
writerThread.start();
readerThread.start();
}
}
执行结果:
// readerThread会正确打印:"Flag is now true"。
Flag set to true
Flag is now true
3. 指令重排序:为什么需要volatile?
3.1 指令重排序可能会导致问题
编译器和处理器为了优化性能,可能会对指令进行重排序。例如,以下代码:
java
int a = 1;
int b = 2;
可能会被重排序为:
java
int b = 2;
int a = 1;
虽然看起来顺序变了,但结果是一样的。然而,在多线程环境下,指令重排序可能会导致问题。
让我们用"厨房里的工作流程"的比喻来理解:
想象一下,你在厨房里做饭,需要先切菜,再炒菜。但厨师(编译器)可能先炒菜,再切菜,结果菜还没切好就炒了。
在多线程环境中,指令重排序可能导致线程看到不一致的状态。
3.2 代码示例3:指令重排序问题演示
java
public class ReorderingExample {
private int a = 0;
private int b = 0;
public void writer() {
a = 1;
b = 2;
}
public void reader() {
int x = b;
int y = a;
System.out.println("x: " + x + ", y: " + y);
}
public static void main(String[] args) {
ReorderingExample example = new ReorderingExample();
Thread writerThread = new Thread(() -> example.writer());
Thread readerThread = new Thread(() -> example.reader());
writerThread.start();
readerThread.start();
}
}
执行结果:可能输出" x: 2, y: 0"或"x: 2, y: 1"(由于指令重排序)。
3.3 代码示例4:使用volatile解决指令重排序问题
java
public class ReorderingExampleWithVolatile {
private volatile int a = 0;
private volatile int b = 0;
public void writer() {
a = 1;
b = 2;
}
public void reader() {
int x = b;
int y = a;
System.out.println("x: " + x + ", y: " + y);
}
public static void main(String[] args) {
ReorderingExampleWithVolatile example = new ReorderingExampleWithVolatile();
Thread writerThread = new Thread(() -> example.writer());
Thread readerThread = new Thread(() -> example.reader());
writerThread.start();
readerThread.start();
}
}
执行结果:总是输出" x: 2, y: 1"。
4. volatile与synchronized的对比
4.1 二者的对比
| 特性 | volatile | synchronized |
|---|---|---|
| 内存可见性 | 保证 | 保证 |
| 原子性 | 不保证 | 保证 |
| 指令重排序 | 禁止 | 禁止 |
| 锁机制 | 无锁 | 基于锁 |
| 性能 | 高(轻量级) | 低(重量级) |
| 适用场景 | 状态标志、单例模式等 | 多个操作的原子性 |
用"超市收银台"的比喻来理解:
- volatile:就像超市里的"状态显示屏",显示商品是否在库存中(内存可见性),但不负责处理购物(原子性)。
- synchronized:就像超市里的"收银员",负责处理整个购物过程(原子性),包括扫描商品、计算价格、收钱等。
4.2 代码示例5:volatile与synchronized的对比
java
public class VolatileVsSynchronized {
private volatile int counter = 0;
private int synchronizedCounter = 0;
public void incrementVolatile() {
counter++;
}
public synchronized void incrementSynchronized() {
synchronizedCounter++;
}
public static void main(String[] args) {
VolatileVsSynchronized example = new VolatileVsSynchronized();
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.incrementVolatile();
example.incrementSynchronized();
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Volatile counter: " + example.counter);
System.out.println("Synchronized counter: " + example.synchronizedCounter);
}
}
执行结果:
Volatile counter: 9938
Synchronized counter: 10000
注意:volatile counter的值可能小于10000,因为counter++不是原子操作。而synchronized counter的值总是10000,因为synchronized保证了原子性。
5. volatile的高级用法
5.1 代码示例6:双重检查锁定(DCL)模式
java
public class DoubleCheckedLocking {
private static volatile DoubleCheckedLocking instance;
private DoubleCheckedLocking() {}
public static DoubleCheckedLocking getInstance() {
if (instance == null) {
synchronized (DoubleCheckedLocking.class) {
if (instance == null) {
instance = new DoubleCheckedLocking();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " -> " + DoubleCheckedLocking.getInstance());
}).start();
}
}
}
执行结果:
所有线程打印的实例地址相同,说明`instance`在多线程环境下被正确初始化。
Thread-1 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-7 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-2 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-4 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-8 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-0 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-5 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-3 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-9 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
Thread-6 -> com.example.javasample.corejava.multithread.volatileSample.DoubleCheckedLocking@2d69d401
5.2 代码示例7:volatile与原子类的组合使用
java
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileAndAtomic {
private volatile boolean flag = false;
private AtomicInteger counter = new AtomicInteger(0);
public void update() {
flag = true;
counter.incrementAndGet();
}
public void check() {
if (flag) {
System.out.println("Counter: " + counter.get());
}
}
public static void main(String[] args) {
VolatileAndAtomic example = new VolatileAndAtomic();
Thread updater = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.update();
}
});
Thread checker = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.check();
}
});
updater.start();
checker.start();
try {
updater.join();
checker.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果:
Counter: 245
Counter: 1000
Counter: 1000
(后续已忽略)
checker线程能够正确看到flag更新和counter的值。
6. volatile的适用场景和局限性
6.1 适用场景
- 状态标志 :如
flag变量,表示某个操作是否完成 - 单例模式 :双重检查锁定(DCL)中保证
instance的可见性 - 读多写少的场景:如缓存、配置项
- 简单的状态同步:如"数据加载完成"的标志
6.2 局限性
- 不保证原子性 :如
counter++不是原子操作,需要使用AtomicInteger - 不适用于复合操作 :如
if (condition) { doSomething(); }需要同时保证条件判断和操作的原子性 - 不适用于多线程同时修改同一变量 :如果多个线程需要同时修改同一个变量,应该使用
AtomicInteger或synchronized - 不保证操作的顺序性 :即使使用
volatile,多个操作之间仍然可能被重排序
7. 总结
volatile关键字是Java并发编程中一个重要的工具,它通过保证内存可见性和禁止指令重排序来确保多线程环境下的数据一致性。然而,它并不保证原子性,所以在使用时需要谨慎。
- 内存可见性 :
volatile确保一个线程对变量的修改对其他线程立即可见 - 指令重排序 :
volatile禁止指令重排序,保证操作的顺序 - 适用场景:状态标志、单例模式、读多写少的场景
- 局限性:不保证原子性,不适用于复合操作
在实际开发中,我们应该根据具体场景选择合适的同步机制。如果只需要保证内存可见性,volatile是最佳选择;如果需要保证原子性,应该使用synchronized或原子类。
8. 往期回顾
【Java线程安全实战】⑧ 阶段同步的艺术:Phaser 与 Condition 的高阶玩法
【Java线程安全实战】⑨ CompletableFuture的高级用法:从基础到高阶,结合虚拟线程
【Java线程安全实战】⑩ 信号量的艺术:Semaphore 如何成为系统的"流量阀门"?
【Java线程安全实战】11 深入线程池的5种创建方式:FixedThreadPool vs CachedThreadPool vs ScheduledThreadPool
【Java线程安全实战】12 Exchanger的高级用法:快递站里的"双向交接点"
9. 经典书籍推荐
《Java并发编程实战》(Java Concurrency in Practice)作者:Brian Goetz 等
- 地位:Java并发领域的"圣经"
- 价值:详细讲解了
volatile的原理和使用场景,理论扎实,实践指导性强 - 出版时间:2006年(虽然出版较早,但其核心原理至今仍是金科玉律)
10. 下一篇文章预告
在掌握了volatile的奥秘后,我们将在下一篇文章中深入探讨ForkJoinPool。我们将从Java 7引入的并行计算框架出发,通过生动的示例,揭示ForkJoinPool如何在分治算法(如归并排序、快速排序)中实现高效的并行计算。我们将对比ForkJoinPool与普通线程池的区别,分析其工作原理和适用场景,并探讨如何在实际项目中优化并行计算性能。敬请期待!
11. 附录:volatile的底层原理
volatile的底层原理主要依赖于Java内存模型(JMM)和CPU的内存屏障(Memory Barrier)。
-
内存屏障 :
volatile在编译时会插入内存屏障指令,防止指令重排序。- 读屏障(Load Barrier):在读取
volatile变量前插入,确保读取到最新的值 - 写屏障(Store Barrier):在写入
volatile变量后插入,确保写入的值对其他线程可见
- 读屏障(Load Barrier):在读取
-
Java内存模型 :JMM规定了
volatile变量的读写行为:- 读
volatile变量时,会从主内存中读取最新值 - 写
volatile变量时,会立即刷新到主内存
- 读
-
CPU指令 :在x86架构中,
volatile通常通过LOCK前缀指令实现,确保对内存的操作是原子的。
通过这些机制,volatile确保了变量的内存可见性和禁止指令重排序,为多线程编程提供了基础保障。