synchronized
和 volatile
是 Java 中用于处理并发编程的两个重要关键字,它们的作用和适用场景有明显区别,但也有一定的互补性。以下是两者的详细对比:
核心区别
特性 | synchronized |
volatile |
---|---|---|
原子性 | 保证代码块或方法在同一时刻只能被一个线程访问,从而保证操作的原子性。例如:synchronized (this) { count++; } |
仅保证变量的单次读/写操作是原子的(如 volatile int count ),但复合操作(如 count++ )不保证原子性。 |
可见性 | 通过解锁时刷新本地内存到主内存,加锁时从主内存读取最新值,保证可见性。 | 通过内存屏障(Memory Barrier)强制刷新变量到主内存,并使其他线程的本地缓存失效,保证可见性。 |
有序性 | 通过锁的获取和释放建立 Happens-Before 关系,禁止指令重排序。 | 禁止特定类型的指令重排序(如写操作前的指令不能重排序到写操作之后)。 |
使用范围 | 可修饰方法、代码块。 | 只能修饰变量。 |
性能开销 | 涉及锁的获取和释放,可能导致线程阻塞,性能开销较大。 | 仅通过内存屏障实现,无锁竞争,性能开销较小。 |
线程阻塞 | 会导致未获取锁的线程阻塞。 | 不会导致线程阻塞。 |
底层实现
synchronized
:- 依赖对象头中的 Monitor(监视器)实现,通过锁升级(偏向锁 → 轻量级锁 → 重量级锁)优化性能。
- 涉及用户态与内核态的切换(重量级锁),成本较高。
volatile
:- 通过 JVM 插入内存屏障(如
StoreLoad
屏障)实现:- 写操作后插入屏障,确保写操作前的所有操作都已完成,并刷新到主内存。
- 读操作前插入屏障,确保其他线程的写操作已刷新到主内存。
- 通过 JVM 插入内存屏障(如
典型应用场景
场景 | synchronized 示例 |
volatile 示例 |
---|---|---|
状态标志 | java<br>public synchronized void setFlag(boolean flag) {<br> this.flag = flag;<br>}<br> |
java<br>private volatile boolean flag = false;<br> |
复合操作原子性 | java<br>public synchronized int increment() {<br> return count++;<br>}<br> |
不适用(count++ 非原子)。 |
单例模式 | java<br>public static synchronized Singleton getInstance() { ... }<br> |
java<br>private static volatile Singleton instance;<br> |
发布不可变对象 | java<br>public synchronized void publish() {<br> this.obj = new ImmutableObject(...);<br>}<br> |
java<br>private volatile ImmutableObject obj;<br> |
结合使用示例
双重检查锁定(Double-Checked Locking)单例模式:
public 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;
}
}
- 关键点:
volatile
防止new Singleton()
的指令重排序(如先分配内存再初始化对象),避免其他线程看到半初始化的对象。synchronized
保证只有一个线程能创建实例。
选择建议
仅需可见性:使用
volatile
,如状态标志位。private volatile boolean shutdown = false; public void shutdown() { shutdown = true; } public void run() { while (!shutdown) { ... } // 保证立即看到其他线程修改的 shutdown 值 }
- 需原子性+可见性:使用
synchronized
或ReentrantLock
,如计数器。public synchronized void increment() { count++; }
- 复合操作:
volatile
不适用,需使用锁或原子类(如AtomicInteger
)。private final AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } // 原子操作
总结
需求 | synchronized |
volatile |
Atomic* |
---|---|---|---|
原子性 | ✅ | ❌ | ✅ |
可见性 | ✅ | ✅ | ✅ |
有序性 | ✅ | ✅ | ✅ |
无锁实现 | ❌ | ✅ | ✅ |
低延迟(高并发场景) | ❌ | ✅ | ✅ |
- 优先使用
volatile
:当仅需保证变量可见性且不涉及复合操作时。 - 使用
synchronized
:当需要保证原子性、可见性和有序性,且并发度不高时。 - 考虑原子类:在高并发场景下,对单个变量的复合操作优先使用
Atomic*
类(如AtomicInteger
)。