Skip to content

synchronized / Lock / volatile

在总览中的位置Java 并发模型总览 §3
前置基础CAS 与无锁原子基础
相关主题Atomic* / CAS / LongAdder · JMM:happens-before / 安全发布

一句话定位

这三者不是同义词:synchronized 解决互斥 + 可见性Lock 提供更强的等待/中断/条件控制volatile 解决可见性 + 有序性约束,但不提供复合操作原子性。

代码索引

主题Lab 说明源码
volatile vs AtomicInteger vs synchronized 行为对比volatile-vs-atomic-vs-lockVolatileVsAtomicVsLock.java
volatile 在 DCL 单例中的安全发布作用singleton-dclSingletonDcl.java

为什么需要

只要多个线程会同时碰到同一份状态,你就必须先回答三个问题:

  1. 要不要互斥?——同一时刻能不能允许两个线程都进来改?
  2. 要不要最新值可见?——一个线程改完,另一个线程什么时候能看到?
  3. 要不要更强控制?——是否需要超时尝试、可中断等待、公平性、多条件等待?

这也是为什么“我给变量加个 volatile 就线程安全了”几乎总是错的。真实工程里:

  • Android 主线程和后台线程共用状态时,不仅要避免脏数据,还要避免主线程被锁卡住。
  • Backend 多线程更新缓存 / 状态机时,不仅要保证结果对,还要保证延迟可控。
  • Kotlin/Flutter 团队切到 Java 老代码时,最容易把共享内存问题误解成“像协程/Promise 一样只关心异步顺序”。

底层机制

1. synchronized:默认首选的内置监视器锁

Synchronized 的价值在于:语义直接、自动释放、JVM 优化成熟

java
private final Object lock = new Object();
private int cacheVersion = 0;

void bumpVersion() {
    synchronized (lock) {
        cacheVersion++;
    }
}

它提供:

  • 互斥:同一把锁保护的临界区,一次只允许一个线程进入
  • 可见性:释放锁前的写入,对随后获取同一把锁的线程可见
  • 结构安全:锁在异常时也会自动释放,不容易漏 unlock()

适合:

  • 临界区短
  • 锁边界清晰
  • 不需要超时尝试、公平策略、可中断获取

不适合:

  • 临界区里包着 IO、RPC、数据库访问等长耗时操作
  • 需要多个等待条件队列的复杂协调

2. Lock / ReentrantLock:当你需要更强控制

java
ReentrantLock lock = new ReentrantLock();

void update() {
    lock.lock();
    try {
        doUpdate();
    } finally {
        lock.unlock();
    }
}

ReentrantLock 相比 synchronized 多出来的能力包括:

  • tryLock():尝试拿锁,拿不到就降级或返回
  • tryLock(timeout, unit):有超时地等待
  • lockInterruptibly():拿锁等待过程中可响应中断
  • Condition:比 wait/notify 更明确的条件队列
  • 可选公平锁

什么时候值得上 Lock

  • 你必须避免无限等待
  • 需要在等待锁时可取消
  • 需要多个条件队列协调生产者 / 消费者 / 状态机

什么时候别为了“高级感”硬上:

  • 只是简单互斥
  • 团队维护成本高于收益
  • 没有充足测试去覆盖复杂控制流

3. volatile:轻量可见性与重排序约束,不是轻量锁

java
private volatile boolean stopped = false;

void stop() {
    stopped = true;
}

void loop() {
    while (!stopped) {
        doOneRound();
    }
}

volatile 适合:

  • 单写多读的状态标记位:停止开关、熔断开关、初始化完成标志
  • 不可变快照引用切换:整体替换配置对象,而不是逐字段修改
  • DCL 单例:禁止“引用先可见、构造尚未完成”的重排序

volatile 不适合:

  • count++ 这类读改写复合操作
  • 多字段必须一起保持一致的状态更新
  • 想用它代替真正互斥的场景

经典反例:

java
private volatile int count = 0;

void wrong() {
    count++; // ❌ 仍然不是原子操作
}

4. 默认选型顺序

先问自己要解决什么:

你要解决的问题优先选型
只有可见性 / 开关位 / 快照引用替换volatile
一个短小临界区的互斥与可见性synchronized
要超时、可中断、条件队列ReentrantLock
单变量原子计数 / 状态切换Atomic* / CAS / LongAdder

5. Android 场景下的额外边界

  • 主线程拿锁前,先假设自己绝不能等很久
  • 不要把网络、磁盘、大 JSON 解析放进锁内。
  • 如果你在 Kotlin 业务层主要用协程,很多时候更适合用不可变状态对象 + StateFlow / Mutex,但底层 Java SDK、旧模块仍要看得懂 synchronized / Lock / volatile

Android / Flutter / Web / Backend 对照

维度synchronized / Lock / volatileAndroidFlutterWebBackend
共享模型共享内存主线程 + 后台线程共享对象很常见isolate 默认不共享内存主线程 JS 单线程,Worker 用消息多线程共享缓存/队列/连接池
典型原语锁、条件队列、可见性标志同上,额外叠加生命周期和 ANR 风险更多用消息、FutureStream更多靠事件顺序和 AbortController锁、CAS、并发容器是日常
最大误区volatile 当线程安全万能钥匙在主线程持有锁做耗时工作把 Java 锁思维硬套到 isolate把事件顺序错当共享内存同步把锁范围放到 RPC / DB 外围

常见场景

1. Android 后台任务停止开关

对应 Labvolatile-vs-atomic-vs-lock

java
private volatile boolean cancelled = false;

void cancel() {
    cancelled = true;
}

适合条件:

  • 一个线程写,多线程读
  • 状态本身独立,不和其他字段要求原子联动

不适合条件:

  • 取消时还必须同时更新多个字段,并保证一致性

2. 多字段状态一起更新

比如“列表数据 + loading 状态 + 错误文案”要一起切换,不能只靠 volatile

java
private final Object lock = new Object();
private UiState state = UiState.idle();

void onDataLoaded(List<Item> items) {
    synchronized (lock) {
        state = new UiState(false, items, null);
    }
}

如果你把 isLoadingitemserror 分成多个独立变量分别更新,就很容易让别的线程读到“不成套”的中间态。

3. 需要超时尝试拿锁的后端状态机

java
if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
    try {
        flushBatch();
    } finally {
        lock.unlock();
    }
} else {
    metrics.recordBusyDrop();
}

适合:

  • 不能无限等锁
  • 锁拿不到时允许降级 / 丢弃 / 延后

4. DCL 单例里的 volatile

对应 Labsingleton-dcl

java
class Singleton {
    private static volatile Singleton instance;

    static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这里的 volatile 不是为了互斥,而是为了安全发布:防止别的线程读到半初始化对象。

常见坑

现象修法
volatile 修饰计数器后仍然 count++结果偶发偏小AtomicInteger / 锁
把 IO / RPC / 磁盘操作放进 synchronized线程长时间阻塞,主线程容易卡顿缩小临界区,只保护共享状态
Lock 却忘记 finally unlock()偶发永久卡死始终 try/finally
多把锁获取顺序不一致线上偶发死锁统一锁顺序,必要时用 tryLock + 超时
一上来就用公平锁吞吐下降、延迟抖动默认先用非公平,除非业务真的需要强公平
DCL 漏掉 volatile偶发半初始化对象DCL 必须 volatile,或改用静态内部类 / enum

与相近概念对比

概念保证什么最适合最不适合
volatile可见性 + 部分有序性标志位、快照引用切换、DCL读改写原子操作、多字段一致性
synchronized互斥 + 可见性短临界区、默认互斥需求超时获取、复杂条件队列
ReentrantLock互斥 + 可中断/超时/条件队列更复杂协调、精细控制简单锁也硬上,导致复杂度过高
ReadWriteLock读写分离读多写少、临界区较大写多或读写转换复杂场景
Atomic*单变量原子更新计数器、状态位切换多字段事务性一致性

对应实验

Lab说明源码
volatile-vs-atomic-vs-lockvolatile-vs-atomic-vs-lockVolatileVsAtomicVsLock.java
singleton-dclsingleton-dclSingletonDcl.java

当前仍缺:ReentrantLock / Condition / tryLock(timeout) / 公平锁吞吐差异的专属实验。

复习检查题

  1. volatile 为什么能保证“看见最新值”,却保证不了 count++ 正确?

    :因为 volatile 只保证读写可见性和部分有序性,不保证“读-改-写”这三个步骤合成一个原子动作。count++ 在并发下仍可能发生更新丢失。

  2. 什么情况下 synchronized 往往比 ReentrantLock 更合适?

    :当需求只是一个短小、边界清晰的互斥临界区,而且不需要超时尝试、可中断等待或多个条件队列时,synchronized 语义更直接、出错面更小。

  3. 为什么说 Lock 的优势不在“更高级”,而在“更可控”?

    :因为它提供 tryLock()lockInterruptibly()Condition、公平策略等控制能力,能表达 synchronized 不方便表达的协调需求;但同时也增加了忘记释放锁和控制流复杂化的风险。

  4. Android 主线程上使用锁时最应该警惕什么?

    :最该警惕长时间等待和把耗时操作包进锁里。主线程一旦被后台线程持有的锁阻塞,就会出现掉帧、卡顿甚至 ANR。

  5. DCL 单例为什么一定要给 instancevolatile

    :因为对象创建可能被重排序成“先把引用写出去,再慢慢初始化对象内部”。没有 volatile 时,其他线程可能读到非空但尚未初始化完成的半成品对象。

速记

  • 先问自己要的是互斥、可见性,还是更强控制。
  • 默认先想 synchronized,复杂协调再上 Lock
  • volatile 不是轻量锁;它最擅长标志位和快照引用。
  • 主线程不要长时间等锁,锁里不要包耗时 IO。