并发编程核心特性解析:volatile与synchronized的应用
在多线程环境下,正确实现程序逻辑远比单线程复杂。由于线程之间共享资源并可能同时访问同一数据,因此必须深入理解并发编程中的关键特性。其中,共享性、互斥性、可见性与原子性是保障线程安全的四大基石。本文聚焦于可见性与原子性的实现机制,重点分析 volatile 与 synchronized 的作用原理。
内存模型概览
Java 内存模型(JMM)定义了线程如何通过主内存与本地工作内存交互。每个线程拥有独立的工作内存,变量副本存储于其中。当线程修改变量时,首先更新本地缓存,而主内存中的值可能滞后,从而引发可见性问题。
可见性保障机制
1. 使用 volatile 关键字
volatile 保证被修饰的变量在修改后立即写回主内存,并且每次读取都从主内存获取最新值。它不提供锁机制,也不保证复合操作的原子性,但能有效解决"一个线程修改,其他线程看不到"的问题。
- 适用于状态标志位、开关控制等场景。
- 对引用类型仅保证引用地址的可见性,不保证对象内部字段的更新可见。
2. 使用 synchronized 同步块 当线程释放锁(unlock)时,会强制将所有本地缓存中被修改的数据刷新至主内存。进入同步块前,线程需从主内存重新加载变量值。这一过程天然具备可见性保障。
public class Counter {
private volatile int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getValue() {
return count;
}
}
原子性实现方式
原子性指一个操作不可中断,要么全部完成,要么完全不执行。例如 i++ 实际上包含三个步骤:读取 → 增加 → 写回,这三步在多线程下可能交错执行,导致结果错误。
如何确保原子性?
1. synchronized 关键字 通过加锁限制同一时间只有一个线程可执行临界区代码。整个代码块被视为一个原子操作单位。
2. JUC 工具包中的 Lock 接口 如 ReentrantLock 等,提供了更灵活的锁控制,同样可以包裹任意代码段以实现原子性。
import java.util.concurrent.locks.ReentrantLock;
public class AtomicCounter {
private int value = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
value++;
} finally {
lock.unlock();
}
}
public int getValue() {
return value;
}
}
小结
- volatile 适合轻量级可见性需求,不支持原子性。
- synchronized 与 Lock 提供可见性与原子性双重保障,适用于需要严格一致性的共享资源操作。
- 在实际开发中,应根据场景选择合适的同步手段:简单状态管理可用 volatile;复杂共享状态则推荐使用锁机制。