同步与可见性
约 901 字大约 3 分钟
布欧-Lewyon
2026-05-16
多线程访问共享变量时可能遇到两个核心问题:竞态条件(数据被同时修改)和可见性(一个线程的修改对另一线程不可见)。synchronized 和 volatile 是解决这些问题的最基础手段。
竞态条件
public class Counter {
private int count = 0;
public void increment() {
count++; // 不是原子操作:读取 → 加1 → 写入
}
public int getCount() {
return count;
}
}两个线程同时调用 increment() 时,可能都读取到旧值,各自加 1 后写回,实际只加了 1 而不是 2。这就是经典"丢失更新"的竞态条件。
synchronized
synchronized 保证同一时刻只有一个线程能进入被保护的代码块或方法,同时保证进入前能看到前一个线程对共享变量的所有修改。
同步方法
public class SafeCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}synchronized 方法使用的锁是当前实例对象(this)。静态 synchronized 方法使用 Class 对象作为锁。
同步代码块
当只需要保护方法中的部分代码时,同步代码块比整个方法同步性能更好:
public class SafeList {
private final List<String> list = new ArrayList<>();
private final Object lock = new Object(); // 专用锁对象
public void add(String item) {
synchronized (lock) {
list.add(item);
}
}
public int size() {
synchronized (lock) {
return list.size();
}
}
}死锁
两个线程各自持有对方需要的锁,互相等待永远不会释放:
Object lockA = new Object();
Object lockB = new Object();
// 线程 1
synchronized (lockA) {
Thread.sleep(100);
synchronized (lockB) { ... }
}
// 线程 2
synchronized (lockB) {
Thread.sleep(100);
synchronized (lockA) { ... }
}避免死锁的方法:始终以固定的全局顺序获取锁(如按 lockA → lockB 的顺序),或使用 tryLock 超时。
volatile
volatile 解决可见性问题:对一个 volatile 变量的写入会立即对其他线程可见,且禁止编译器和 CPU 对其进行指令重排序。
public class RunningFlag {
private volatile boolean running = true;
public void stop() {
running = false; // 对其他线程立即可见
}
public void run() {
while (running) { // 不会因缓存而无限循环
// 工作
}
}
}如果没有 volatile,run() 线程可能因为 CPU 缓存而永远看不到 running = false 的修改。
volatile 的局限
- 不保证原子性:
count++用volatile仍存在竞态条件(读取-修改-写入三步)。 - 适合一个线程写、多个线程读的场景,或单纯的状态标志。
happens-before 规则
JMM(Java 内存模型)规定了几种 happens-before 关系,理解它们有助于判断多线程下的安全性:
- 程序次序规则:一个线程内的代码,写在前面的
happens-before后面的。 - 锁规则:
unlock()happens-before后续的lock()。 - volatile 规则:对
volatile变量的写happens-before后续的读。 - 传递性:如果 A
happens-beforeB,Bhappens-beforeC,则 Ahappens-beforeC。
小结
synchronized保证互斥与可见性,适合更新复合操作(count++)。volatile保证可见性与禁止重排序,不保证原子性,适合状态标志或单次读写。- 死锁因锁顺序不一致导致;预防办法:固定加锁顺序,或使用
Lock.tryLock()。 - JMM happen-before 规则是理解多线程安全的理论基础。
- 易错:
volatile不能替代synchronized解决count++的问题。常见误解是给变量加volatile就线程安全了——实际上复合操作仍需要synchronized或AtomicInteger。 - 思考任务:用
synchronized实现一个线程安全的BoundedBuffer(有界缓冲区),支持put和take方法。
上一节:线程与线程池
下一节:JUC 工具类
