--- title: "02-synchronized与volatile" created: 2026-09-01 tags: - Java --- # synchronized与volatile > [!note] 本篇定位 > 并发安全的两大关键字:**synchronized 解决"互斥"**(同一时刻只有一个线程进临界区),**volatile 解决"可见性与有序性"**(但不保证原子性)。本篇的地基是 [[04-JMM与内存可见性|JMM 与内存可见性]]——先理解"工作内存/主内存"模型再回来,所有现象都解释得通。 ## 先看问题:不加锁会怎样 ```java public class UnsafeCounter { static int count = 0; public static void main(String[] args) throws Exception { Runnable r = () -> { for (int i = 0; i < 10000; i++) count++; // 非原子:读→加→写 }; Thread t1 = new Thread(r), t2 = new Thread(r); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); // 常见输出 12000~18000,几乎不会是 20000 } } ``` `count++` 是三条字节码指令(读字段、加 1、写字段),两个线程交错执行就会互相覆盖——这就是**竞态条件(Race Condition)**。 ## synchronized:三种用法,锁的是同一个东西吗 ```java public class SyncDemo { // 1. 修饰实例方法:锁的是当前实例 this public synchronized void instanceMethod() { } // 2. 修饰静态方法:锁的是 Class 对象(全局唯一) public static synchronized void staticMethod() { } // 3. 修饰代码块:锁的是括号里的对象 private final Object lock = new Object(); public void blockMethod() { synchronized (lock) { // 临界区 } } } ``` **核心认知**:synchronized 是**可重入的互斥锁**,任何对象都可以当锁(对象头 Mark Word 记录锁状态)。注意——两个线程用**不同的锁对象**就互不排斥,这既是灵活性也是 bug 来源。 **字节码层**:同步代码块靠 `monitorenter/monitorexit` 指令,同步方法靠 ACC_SYNCHRONIZED 标志位;每个对象关联一个 monitor(管程),线程抢的其实就是 monitor 的持有权。 ## 锁升级(JDK 6 优化,面试必考) 对象锁状态随竞争激烈程度**单向升级**:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。 | 级别 | 适用场景 | 做法 | | --- | --- | --- | | 偏向锁 | 始终只有一个线程访问 | 在对象头记下线程 ID,之后该线程进入无需任何同步操作 | | 轻量级锁 | 多线程交替执行、无实质竞争 | 栈上锁记录 + CAS 把对象头指向栈帧;失败则自旋几次 | | 重量级锁 | 竞争激烈 | 依赖操作系统 mutex,涉及用户态/内核态切换,线程阻塞挂起 | 注意:JDK 15 起偏向锁默认禁用并逐步移除(维护成本高于收益),面试提一句"新版本已弃用偏向锁"是加分项。 ## 死锁:锁的极端后果(面试必考) > [!note] 本节定位 > 讲完"锁怎么用",必须讲"锁用错了会怎样"。死锁是 synchronized 互斥性的直接代价,四条件+避免手段+现场排查是面试三板斧。 **一、两分钟复现一个死锁** ```java public class DeadLockDemo { static final Object A = new Object(); static final Object B = new Object(); public static void main(String[] args) throws Exception { new Thread(() -> { synchronized (A) { Thread.sleep(100); // 给对方抢到 B 的时间窗 synchronized (B) { System.out.println("t1 拿到 A+B"); } } }, "t1").start(); new Thread(() -> { synchronized (B) { Thread.sleep(100); synchronized (A) { System.out.println("t2 拿到 B+A"); } } }, "t2").start(); } } ``` t1 持有 A 等 B,t2 持有 B 等 A——两边永远等下去。**加锁顺序不一致**就是最典型的死锁成因。 **二、产生的四个必要条件(Coffman 条件,缺一不可)** | 条件 | 含义 | 对应的破坏手段 | | --- | --- | --- | | 互斥 | 资源同一时刻只能被一个线程持有 | 不可破坏(锁的本义) | | 持有并等待 | 拿着一个锁的同时去等另一个 | **一次性申请所有资源**(把多把锁的获取包进一个大 synchronized 或统一资源管理器) | | 不可剥夺 | 锁只能由持有者主动释放 | synchronized 做不到;**ReentrantLock.tryLock(timeout)** 拿不到就放弃已持有的锁(见 [[2-Learning/04-Java/03-Java并发/04-Lock与AQS|04-Lock与AQS]]) | | 循环等待 | t1 等 t2 的、t2 等 t1 的形成环 | **全局统一加锁顺序**(所有线程都先拿 A 再拿 B,环就成不了) | 四个条件同时成立才死锁——所以工程上只需破坏任意一条。**"固定加锁顺序"是最常用、成本最低的一条**。 **三、定位死锁:现场排查三板斧** 1. **jstack**(最常用):`jstack ` 输出末尾直接给出结论 `Found one Java-level deadlock`,并打印"Found 1 deadlock"的线程互相等待链; 2. **jconsole / jvisualvm**:Threads 页签点"检测死锁"按钮,图形化看到两个线程的 Monitor 状态; 3. **代码级预警**:ReentrantLock 的 `tryLock` + 超时重试,或 `Lock.newCondition()` 带超时的 await——把"永久等待"变成"限时等待"。 顺带认识两个亲戚:**活锁**(线程没阻塞,但互相谦让导致谁也走不下去,如重试时都用相同退避节奏)和**饥饿**(低优先级线程永远抢不到锁,非公平锁的副作用)——三者统称"活跃性问题"。 **四、勾连** - JUC 工具类如何从设计上绕开死锁:BlockingQueue 把"锁+等待"封装进队列,生产者消费者两边永远只碰队列不互相持锁,见 [[05-并发容器与工具类|05-并发容器与工具类]]; - 排查线上问题(含死锁)的系统动作见 [[02-内存溢出|02-内存溢出]] 的线上排查清单。 ## volatile:可见性 + 有序性,不保证原子性 ```java // volatile 的典型正确用法:一写多读的状态标志 public class TaskRunner { private static volatile boolean running = true; public static void main(String[] args) throws Exception { new Thread(() -> { while (running) { /* 干活 */ } System.out.println("看到 running=false,退出"); }).start(); Thread.sleep(100); running = false; // 没有 volatile,子线程可能永远看不到这个修改 } } ``` volatile 三件事: 1. **可见性**:写 volatile 变量立即刷回主内存,读时强制从主内存加载(MESI 缓存一致性协议 + 内存屏障); 2. **禁止指令重排序**:编译器和 CPU 不能把 volatile 读写与前后操作随意换序; 3. **不保证原子性**:`volatile int i; i++` 依然会丢更新——它不是锁。 **happens-before 一句话**:volatile 写 happens-before 后续的 volatile 读——这就是"写线程的修改对读线程可见"的规范表述(JMM 细节见 [[04-JMM与内存可见性|JMM 篇]])。 **双重检查锁单例(DCL)——volatile 最经典的面试题**: ```java public class Singleton { private static volatile Singleton instance; // volatile 必须加 private Singleton() {} public static Singleton getInstance() { if (instance == null) { // 第一次检查:避免每次加锁 synchronized (Singleton.class) { if (instance == null) { // 第二次检查:防止重复创建 instance = new Singleton(); // 三步:分配内存→初始化→赋值引用 } } } return instance; } } ``` 不加 volatile 的 bug:`instance = new Singleton()` 可能被重排为"分配→**赋值引用**→初始化",另一线程在第一次检查时拿到**未初始化完成**的实例。 ## synchronized vs volatile 一句话总结 | 维度 | synchronized | volatile | | --- | --- | --- | | 原子性 | ✅ 保证 | ❌ 不保证 | | 可见性 | ✅ 保证 | ✅ 保证 | | 有序性 | ✅ 保证 | ✅ 保证 | | 阻塞 | 可能阻塞 | 不阻塞 | | 适用 | 写多读少的临界区 | 状态标志、一写多读 | --- ⬅️ [[01-线程基础与生命周期|01-线程基础与生命周期]] 🏠 [[00-Java|00-Java]] ➡️ [[03-线程池|03-线程池]]