文章目录
在Java并发编程中,volatile
和synchronized
都是用于处理多线程环境下共享变量访问的关键字,但它们的工作机制和适用场景有显著区别。下面我将从多个维度详细对比这两者的差异。
1. 基本概念对比
特性 | volatile | synchronized |
---|---|---|
作用范围 | 变量级别 | 代码块或方法级别 |
原子性 | 不保证复合操作的原子性 | 保证整个代码块的原子性 |
可见性 | 保证变量的可见性 | 保证变量的可见性 |
有序性 | 禁止指令重排序 | 保证有序性 |
线程阻塞 | 不会导致线程阻塞 | 可能导致线程阻塞 |
性能 | 更高 | 相对较低 |
2. 内存语义与实现原理
2.1 volatile的内存语义
public class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写volatile变量
}
public void reader() {
if (flag) { // 读volatile变量
// do something
}
}
}
实现原理:
- 写操作:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存
- 读操作:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,直接从主内存读取
内存屏障:
- 在每个volatile写操作前插入StoreStore屏障
- 在每个volatile写操作后插入StoreLoad屏障
- 在每个volatile读操作后插入LoadLoad和LoadStore屏障
2.2 synchronized的内存语义
public class SynchronizedExample {
private int count = 0;
public synchronized void increment() {
count++; // 原子操作
}
public void doSomething() {
synchronized(this) {
// 同步代码块
}
}
}
实现原理:
- 进入同步块前:先清空工作内存→从主内存拷贝变量副本
- 执行同步代码:使用工作内存中的变量副本
- 退出同步块前:将工作内存中的变量值刷新到主内存
锁机制:
- 基于Monitor实现(对象头中的Mark Word)
- 包含偏向锁、轻量级锁、重量级锁的升级过程
3. 可见性保证对比
3.1 volatile的可见性
volatile变量的写操作对于其他线程的读操作是立即可见的。这是因为:
- 写操作后会强制刷新到主内存
- 读操作前会强制从主内存重新加载
- 防止编译器对指令的重排序
3.2 synchronized的可见性
synchronized块提供的可见性保证更强:
- 进入同步块前:会清空工作内存,从主内存重新加载变量
- 退出同步块时:会把工作内存中的变量强制刷新到主内存
- 遵循"对一个变量解锁前,必须先把此变量同步回主内存"规则
4. 原子性保证对比
4.1 volatile的非原子性
volatile不能保证复合操作的原子性。例如:
private volatile int count = 0;
// 线程不安全!
public void increment() {
count++; // 实际上是read-modify-write三步操作
}
count++
操作实际上分为三步:
- 读取count的值
- 将值加1
- 写回count
在多线程环境下,这三步操作可能被其他线程打断。
4.2 synchronized的原子性
synchronized可以保证代码块的原子性:
private int count = 0;
public synchronized void increment() {
count++; // 原子操作
}
同一时间只有一个线程能执行同步方法/代码块,因此可以保证复合操作的原子性。
5. 有序性保证对比
5.1 volatile的有序性
volatile通过内存屏障禁止指令重排序:
- 写操作前的指令不会被重排序到写操作之后
- 读操作后的指令不会被重排序到读操作之前
典型应用:单例模式的双重检查锁定
class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
5.2 synchronized的有序性
synchronized保证:
- 同步块内的指令不会被重排序到同步块外
- 同步块内的指令可以重排序,但不会影响执行结果(as-if-serial语义)
6. 性能对比
指标 | volatile | synchronized |
---|---|---|
上下文切换 | 无 | 有可能导致线程阻塞和切换 |
编译优化 | 较少限制 | 较多限制 |
适用场景 | 单一变量的可见性控制 | 复杂操作的原子性控制 |
volatile的性能通常比synchronized高,因为:
- 不会导致线程阻塞
- 没有锁的获取和释放开销
- JVM对volatile的优化更好
7. 典型使用场景
7.1 volatile适用场景
- 状态标志位:
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
public void doWork() {
while (!shutdownRequested) {
// 执行任务
}
}
- 一次性安全发布:
class Resource {
private volatile static Resource instance;
public static Resource getInstance() {
if (instance == null) {
instance = new Resource();
}
return instance;
}
}
- 独立观察:
volatile double temperature;
// 多个线程读取温度值,一个线程更新温度值
7.2 synchronized适用场景
- 复合操作:
private int counter;
public synchronized void increment() {
counter++;
}
- 需要互斥访问的资源:
public class BankAccount {
private double balance;
public synchronized void deposit(double amount) {
balance += amount;
}
public synchronized void withdraw(double amount) {
balance -= amount;
}
}
- 对象控制流:
public class BoundedBuffer {
private final Object lock = new Object();
private int count, putIndex, takeIndex;
private Object[] items = new Object[100];
public void put(Object x) throws InterruptedException {
synchronized(lock) {
while (count == items.length)
lock.wait();
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
++count;
lock.notifyAll();
}
}
}
8. 选择建议
根据需求选择合适的关键字:
使用volatile当:
- 变量只有一个线程写,多个线程读
- 变量不参与不变式约束
- 变量不需要与其他变量共同参与原子操作
- 只需要保证变量的可见性
使用synchronized当:
- 操作需要原子性保证
- 多个变量需要作为一个原子单元操作
- 需要控制代码块的执行顺序
- 需要实现线程间的协作(wait/notify)
9. 常见误区
-
认为volatile能替代synchronized:
- volatile不能保证复合操作的原子性
- 例如i++这样的操作仍需同步
-
过度使用synchronized:
- 在不必要时使用会导致性能下降
- 应考虑更轻量级的并发控制方式
-
忽视volatile的有序性:
- volatile不仅能保证可见性,还能防止指令重排序
10. 组合使用示例
在实际开发中,两者经常组合使用:
public class CheesyCounter {
private volatile int value;
private final Object lock = new Object();
public int getValue() {
return value; // volatile读
}
public void increment() {
synchronized(lock) {
value++; // 同步保证原子性
}
}
}
这种组合:
- 读操作使用volatile保证可见性
- 写操作使用synchronized保证原子性
- 兼顾了性能和正确性
总结
volatile和synchronized是Java并发编程中的基础构建块,理解它们的区别对于编写正确且高效的多线程程序至关重要。volatile提供了轻量级的可见性和有序性保证,而synchronized提供了更重量级但更全面的原子性、可见性和有序性保证。开发者应根据具体场景选择合适的关键字,有时还需要组合使用它们以达到最佳效果。