synchronized原理与使用详解

synchronized 原理与使用

这份文档把 synchronized 讲到"你能讲清楚底层 + 能写对代码"的程度:对象头、Monitor、锁升级、wait/notify、可见性、以及工程里的坑。


1. synchronized 是什么?到底锁住了谁?

synchronizedJVM 级别的互斥锁(Monitor Lock),用于保证:

  1. 互斥(Mutual Exclusion):同一时刻只有一个线程进入临界区
  2. 可见性(Visibility):释放锁前对共享变量的写入,对之后获取同一把锁的线程可见(happens-before)
  3. 有序性(Ordering):临界区内的读写不会被 JVM/CPU 随意重排到临界区外

1.1 三种写法与"锁对象"

(1) 同步实例方法
java 复制代码
public synchronized void inc() { ... }

锁住的是:当前实例对象 this

(2) 同步静态方法
java 复制代码
public static synchronized void inc() { ... }

锁住的是:Class 对象XXX.class

(3) 同步代码块
java 复制代码
synchronized (lock) { ... }

锁住的是:括号里的 lock 引用指向的对象

结论:synchronized 不是"锁代码",是"锁对象"。同一把锁 = 同一个对象。


2. JVM 怎么实现 synchronized?字节码层面看懂它

2.1 同步代码块:monitorenter / monitorexit

编译后会生成:

  • monitorenter:尝试获取监视器(Monitor)
  • monitorexit:释放监视器

而且编译器会插入 异常路径的 monitorexit,保证即使抛异常也能释放锁(类似 finally)。

2.2 同步方法:ACC_SYNCHRONIZED 标记

同步方法不会显式出现 monitorenter 指令,而是在方法的 access flags 上加 ACC_SYNCHRONIZED

JVM 调用该方法时会隐式获取/释放对应 Monitor。


3. Monitor 是啥?对象头又是啥?

3.1 每个 Java 对象都可能"关联一个 Monitor"

HotSpot 里常说:

  • 对象头(Object Header)里有 Mark Word
  • synchronized 的锁信息会编码在 Mark Word
  • 膨胀(Inflate)时会指向一个 Monitor(ObjectMonitor)

3.2 Mark Word(很关键)

Mark Word 是对象头的一部分,里面会存:

  • hashCode(可能)
  • GC 分代年龄
  • 锁状态(无锁 / 偏向 / 轻量 / 重量)
  • 指向锁记录(Lock Record)或 Monitor 的指针等

不同锁状态下,Mark Word 的布局不同(JVM 会复用这块位域)。

直觉类比:Mark Word 像"对象自带的锁信息小本本",能记录当前谁在拿锁、拿的是什么形态。


4. 锁升级(无锁 → 偏向 → 轻量 → 重量)

synchronized 不是一上来就重量级互斥锁。HotSpot 有 锁优化/锁升级,大致顺序:

4.1 偏向锁(Biased Locking)

目标:如果这个锁长期被同一个线程反复获取,连 CAS 都省了。

  • 第一次获取:把线程 ID 记录进对象头(Mark Word)
  • 后续同线程进入:几乎就是"看一眼线程 ID",直接过

何时撤销?

  • 另一个线程也来抢:触发偏向撤销(可能到 safepoint),然后升级为轻量或重量

注意:偏向锁在新版本 JDK 上有过变化(有的版本默认关闭/逐步移除)。具体以你用的 JDK 为准。

4.2 轻量级锁(Lightweight Lock)

目标:低竞争时用 CAS + 自旋,避免线程挂起/唤醒的系统调用成本。

过程(简化):

  1. 线程在栈上创建 Lock Record(锁记录)
  2. CAS 把对象头指向这个 Lock Record(并保存旧的 Mark Word)
  3. 成功:拿到锁
  4. 失败:说明有竞争,可能开始自旋,再不行升级重量级

4.3 重量级锁(Heavyweight Lock)

竞争激烈时,锁会膨胀为 ObjectMonitor:

  • 持有者线程:Owner
  • 竞争队列:EntryList
  • 等待队列:WaitSet(用于 wait/notify)

重量级锁会涉及线程 park/unpark(挂起/唤醒),开销更大但更稳。


5. 自旋与适应性自旋:为什么"等一等"有时更快?

当锁持有时间很短(比如临界区就几条指令),直接把线程挂起/唤醒反而更慢。

所以轻量级锁会:

  • 先自旋几次(空转等待)
  • 如果还拿不到,才升级重量级并阻塞

适应性自旋:JVM 会根据历史竞争情况动态调整自旋次数。


6. synchronized 的内存语义(可见性/有序性)

synchronized 的 happens-before 规则:

  • 解锁(monitorexit) 先行发生于 后续对同一把锁的加锁(monitorenter)

也就是说:

  • 线程 A 在临界区写的共享变量
  • 在 A 释放锁后
  • 线程 B 获取同一把锁进入临界区时一定能看到

这等价于:

  • 进入 synchronized:相当于一个 Acquire 屏障
  • 退出 synchronized:相当于一个 Release 屏障

7. wait/notify/notifyAll:它们和 synchronized 什么关系?

7.1 必须在 synchronized 内调用

wait/notify/notifyAll 是 Object 的方法,但必须持有该对象的 Monitor 才能调用,否则抛 IllegalMonitorStateException

7.2 wait 做了什么?

在持有锁的情况下调用 lock.wait()

  1. 当前线程释放该 lock 的 Monitor
  2. 线程进入该 Monitor 的 WaitSet 等待
  3. 被 notify/notifyAll 唤醒后,线程会去竞争锁
  4. 重新拿到锁后 wait 才返回

7.3 notify vs notifyAll

  • notify():随机唤醒一个等待线程
  • notifyAll():唤醒所有等待线程(但最终还要抢锁)

工程建议:

  • 绝大多数场景优先 notifyAll()(避免"唤醒错人"导致死等)
  • notifyAll() 可能导致惊群效应,需要权衡

7.4 正确姿势:while 而不是 if

java 复制代码
synchronized (lock) {
    while (!condition) {
        lock.wait();
    }
    // condition 满足
}

因为:

  • 可能虚假唤醒(spurious wakeup)
  • 也可能被唤醒时条件已被别的线程改回去了

8. 常见坑(高频踩雷)

8.1 锁对象变了:等于没锁

java 复制代码
synchronized (new Object()) { ... } // 每次都是新锁,等于没锁

8.2 锁住了字符串常量:容易"跨模块串锁"

java 复制代码
synchronized ("LOCK") { ... } // 字符串常量会被驻留(intern),可能全局共享

建议:用私有 final Object 作为锁。

8.3 锁粒度太大:性能直接崩

把 IO、RPC、慢 SQL 放进 synchronized,等于把并发变串行。

8.4 误以为 synchronized 能跨进程

synchronized 只在 同一个 JVM 进程内有效。分布式要用 Redis/ZK/DB 锁等。

8.5 死锁(经典)

两个线程以不同顺序获取两把锁:

java 复制代码
// T1: lockA -> lockB
// T2: lockB -> lockA

解决:统一加锁顺序 / 尽量减少多锁嵌套 / 使用 tryLock(ReentrantLock)


9. 实战:三段"写对就能上线"的代码

9.1 私有锁对象(推荐写法)

java 复制代码
public class Counter {
    private final Object lock = new Object();
    private int x;

    public void inc() {
        synchronized (lock) {
            x++;
        }
    }

    public int get() {
        synchronized (lock) {
            return x;
        }
    }
}

9.2 双重检查单例(DCL)必须配 volatile

java 复制代码
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 可能发生指令重排,别的线程看到"半初始化对象"。

9.3 生产者-消费者(wait/notifyAll)

java 复制代码
import java.util.ArrayDeque;
import java.util.Queue;

public class BlockingQueueSimple<T> {
    private final Object lock = new Object();
    private final Queue<T> q = new ArrayDeque<>();
    private final int cap;

    public BlockingQueueSimple(int cap) { this.cap = cap; }

    public void put(T v) throws InterruptedException {
        synchronized (lock) {
            while (q.size() == cap) {
                lock.wait();
            }
            q.add(v);
            lock.notifyAll();
        }
    }

    public T take() throws InterruptedException {
        synchronized (lock) {
            while (q.isEmpty()) {
                lock.wait();
            }
            T v = q.poll();
            lock.notifyAll();
            return v;
        }
    }
}

10. synchronized vs ReentrantLock 怎么选?

维度 synchronized ReentrantLock
语法 关键字,简单 API,灵活
可中断获取锁 不支持 支持 lockInterruptibly()
超时获取 不支持 支持 tryLock(timeout)
公平锁 基本不提供 可选公平/非公平
条件队列 wait/notify(单一 WaitSet) Condition(可多个条件队列)
性能 现代 JVM 已很强(偏向/轻量/自旋) 高度可控,复杂场景更强

工程建议:

  • 简单互斥:优先 synchronized(少犯错)
  • 需要超时/可中断/多个条件队列:上 ReentrantLock/Condition

11. 面试"讲清楚"模板(30 秒版)

synchronized 锁的是对象 Monitor。同步代码块对应字节码 monitorenter/monitorexit,同步方法是 ACC_SYNCHRONIZED 标记。对象头 Mark Word 记录锁状态,HotSpot 有无锁/偏向/轻量/重量级锁升级机制:低竞争时用偏向或轻量(CAS+自旋),竞争激烈膨胀为重量级 ObjectMonitor,线程会阻塞/唤醒。内存语义上,解锁 happens-before 后续同锁加锁,保证可见性和有序性。wait/notify 必须在持有同一把锁的 synchronized 内调用,wait 会释放锁进入 WaitSet,被唤醒后重新竞争锁。


12. 进一步阅读(你如果要深挖)

  • HotSpot 对象头与 Mark Word
  • ObjectMonitor 结构(Owner、EntryList、WaitSet)
  • JIT 与锁消除/锁粗化(逃逸分析相关)
  • JDK 版本差异(偏向锁的默认策略变化)

相关推荐
这周也會开心8 小时前
JVM逃逸分析与标量替换
jvm
C雨后彩虹10 小时前
synchronized底层原理:JVM层面的锁实现
java·synchronized
爱潜水的小L11 小时前
自学嵌入式day41,数据库
jvm·数据库
Chen不旧11 小时前
Java模拟死锁
java·开发语言·synchronized·reentrantlock·死锁
Fortunate Chen21 小时前
类与对象(下)
java·javascript·jvm
萧曵 丶1 天前
Synchronized 详解及 JDK 版本优化
java·多线程·synchronized
予枫的编程笔记1 天前
深度拆解美团后端一面:从压测体系到 JVM 调优的闭环面试艺术
jvm·面试·职场和发展·java面试·美团面试
短剑重铸之日1 天前
《深入解析JVM》第五章:JDK 8之后版本的优化与JDK 25前瞻
java·开发语言·jvm·后端
代码炼金术士1 天前
认识JVM
运维·服务器·jvm