1. 引言:为什么 synchronized 仍然重要

在 Java 并发编程中,ReentrantLockStampedLockConcurrentHashMap 等工具层出不穷,但 synchronized 从未退场。它是 JVM 内置监视器锁(Monitor) 的语法糖——每个 Java 对象都天然关联一把锁,无需额外分配锁对象,JVM 也对其做了大量针对性优化。

理解 synchronized,需要走过三层:

  1. 语法层:三种写法、字节码、ACC_SYNCHRONIZEDmonitorenter/monitorexit
  2. 语义层:JMM 中的互斥、可见性、有序性,以及 happens-before
  3. 实现层:对象头 Mark Word、锁升级、ObjectMonitor,以及编译器的锁消除/粗化

2. 语言层:三种用法与字节码

2.1 三种 synchronized 写法

public class SyncDemo {

    // 1. 同步实例方法 —— 锁对象是 this
    public synchronized void instanceMethod() {
        // ...
    }

    // 2. 同步静态方法 —— 锁对象是 Class 对象
    public static synchronized void staticMethod() {
        // ...
    }

    // 3. 同步代码块 —— 锁对象由括号内表达式决定
    public void blockMethod() {
        synchronized (this) {
            // ...
        }
    }
}

等价关系:instanceMethod()synchronized (this) { ... } 等价;staticMethod()synchronized (SyncDemo.class) { ... } 等价。

锁对象的选择至关重要:只有多个线程竞争同一把锁,互斥才有意义。synchronized (new Object()) 每次创建新对象,等于没有同步。

2.2 字节码:两条不同的路径

synchronized 在字节码层的实现分为两条路径,不可混为一谈:

形式字节码机制
synchronized 方法方法 access_flagsACC_SYNCHRONIZED(0x0020);JVM 在方法调用/返回时隐式加锁/解锁,不出现 monitorenter/monitorexit
synchronized 代码块编译器插入 monitorenter / monitorexit 指令对

ACC_SYNCHRONIZED 定义于 JVM 规范(HotSpot 中 JVM_ACC_SYNCHRONIZED = 0x0020),仅用于方法的 access flags。HotSpot 通过 AccessFlags::is_synchronized() 识别同步方法:

// accessFlags.hpp
bool is_synchronized() const { return (_flags & JVM_ACC_SYNCHRONIZED) != 0; }

2.3 javap 对比示例

public class BytecodeDemo {
    public synchronized void syncMethod() { }
    public void syncBlock() {
        synchronized (this) { }
    }
}

javap -v -c BytecodeDemo 输出(节选):

同步方法——flags 含 ACC_SYNCHRONIZED,Code 段无 monitor 指令:

public synchronized void syncMethod();
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=0, locals=1, args_size=1
       0: return

同步代码块——显式 monitorenter/monitorexit,且编译器生成 try-finally 结构,保证异常路径也能释放锁:

public void syncBlock();
  Code:
     0: aload_0
     1: dup
     2: astore_1
     3: monitorenter           // 加锁
     4: aload_1
     5: monitorexit            // 正常路径释放
     6: goto          14
     9: astore_2
    10: aload_1
    11: monitorexit            // 异常路径释放
    12: aload_2
    13: athrow
    14: return
  Exception table:
     from    to  target type
         4     6     9   any
         9    12     9   any

两种路径在语义上等价:进入时加锁,退出时(含异常)释放锁。差异在于同步方法的加解锁由 JVM 在方法调用/返回处隐式完成,同步块则由 字节码指令 显式完成。

3. 监视器语义:可重入、wait/notify

3.1 可重入性

synchronized 是可重入锁:同一线程可以多次获取同一把锁,不会自死锁。

JVM 通过 递归计数 实现——重量级锁中 ObjectMonitor_recursions 字段记录重入次数;轻量级锁场景下 displaced_header 置为 NULL 表示递归进入。

public class ReentrantDemo {
    public synchronized void outer() {
        inner();  // 同一线程再次获取 this 上的锁,合法
    }

    public synchronized void inner() {
        // ...
    }
}

3.2 wait / notify / notifyAll

Object.wait()notify()notifyAll() 必须在持有对应对象监视器锁的线程中调用,否则抛出 IllegalMonitorStateException

标准用法(来自 java.lang.Object 文档):

synchronized (obj) {
    while (!condition) {
        obj.wait();   // 释放锁,进入等待;被唤醒后需重新竞争锁
    }
    // 条件满足,执行业务逻辑
}

为什么用 while 而不是 if

  1. 虚假唤醒(spurious wakeup):线程可能在没有 notify 的情况下被唤醒,OS 和 JVM 均不保证只唤醒一次。
  2. 多个等待者notify 只唤醒一个线程,被唤醒时条件可能已被其他线程改变。
  3. wait 返回后需重新持锁:从 wait 返回只意味着重新获得了锁,不意味着条件成立。

wait() 释放锁并进入等待集;notify()_WaitSet 移出一个等待者到入口队列;notifyAll() 移动全部等待者。这些在重量级锁的 ObjectMonitor 中实现(见 §7.5)。

4. JMM(Java 内存模型)与 synchronized

4.1 为什么需要 JMM

多线程环境下,编译器优化、CPU 乱序执行、各级缓存会导致:一个线程的写,另一个线程不一定立即可见。若缺乏形式化规则,并发程序的行为将无法推理。

JMM(JLS 第 17 章)定义了 happens-before(先行发生) 关系:若操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 在排序上先于 B。

4.2 synchronized 的三重保证

保证含义实现层面
互斥性同一时刻至多一个线程持有同一把监视器锁Monitor enter/exit
可见性解锁前的写,对后续加锁线程可见锁释放/获取时的内存同步
有序性临界区内操作不会被重排到锁外编译器/CPU 内存屏障约束

JMM 不保证所有线程的全局统一顺序,只保证 happens-before 链 可达的操作之间的可见性与有序性。

4.3 happens-before 规则

synchronized 直接相关的规则(摘自 JLS 与 java.util.concurrent 包文档):

  1. 程序次序规则:同一线程内,前面的操作 happens-before 后面的操作。
  2. 监视器锁规则:对监视器 M 的 unlock happens-before 后续对 M 的 lock。由于 hb 具有传递性,T1 在 unlock 前的所有操作 happens-before T2 在 lock 后的所有操作
  3. volatile 规则(对比用):对 volatile 变量的写 happens-before 后续对该变量的读(无互斥)。
  4. 线程启动规则Thread.start() happens-before 被启动线程中的任意操作。
  5. 线程终止规则:线程中的所有操作 happens-before 其他线程在该线程上 join() 的成功返回。

hb 传递链示意:

sequenceDiagram
    participant T1 as Thread1
    participant Lock as Monitor
    participant T2 as Thread2
    T1->>Lock: synchronized enter
    T1->>Lock: write sharedVar
    T1->>Lock: synchronized exit
    Note over Lock: unlock hb lock
    T2->>Lock: synchronized enter
    T2->>Lock: read sharedVar

传递链:T1 写 sharedVar →(程序次序)→ T1 unlock →(监视器锁规则)→ T2 lock →(程序次序)→ T2 读 sharedVar。因此 T2 一定能看到 T1 的写。

4.4 可见性问题:无锁 vs 有锁

反例——无 happens-before,可能永远看不到更新:

class Broken {
    boolean flag = false;
    int value = 0;

    void writer() {
        value = 42;
        flag = true;   // 与 reader 无 hb 关系
    }

    void reader() {
        while (!flag) { /* spin */ }
        System.out.println(value);  // 可能输出 0
    }
}

修复——同一把锁建立 hb:

class Fixed {
    private final Object lock = new Object();
    boolean flag = false;
    int value = 0;

    void writer() {
        synchronized (lock) {
            value = 42;
            flag = true;
        }
    }

    void reader() {
        synchronized (lock) {
            if (flag) {
                System.out.println(value);  // 保证看到 42
            }
        }
    }
}

4.5 数据竞争

JLS 定义:两个线程访问同一变量,至少一个是写,且没有 happens-before 关系,即构成数据竞争,属于未定义行为。

synchronized 通过互斥 + hb 消除受保护变量的数据竞争。但需注意边界——不同锁之间没有 hb

synchronized (lockA) { x = 1; }
synchronized (lockB) { int y = x; }  // 不保证看到 1

4.6 synchronized 与 volatile 的 JMM 对比

维度synchronizedvolatile
互斥
可见性是(unlock→lock hb)是(写→读 hb)
适用场景复合操作(check-then-act)单一变量的状态标志
阻塞可能阻塞不阻塞

JLS 指出:volatile 的读写「具有与进入/退出监视器类似的内存一致性效果,但不涉及互斥锁定」。

4.8 有序性

单线程内,JMM 保证 as-if-serial 语义synchronized 块内,编译器和 CPU 不能将块内操作重排到块外。但两个相邻 synchronized 块之间,块外操作仍可能在各自块边界处被重排。

4.9 小结

synchronized 的本质是通过监视器锁规则在 unlock 与后续 lock 之间建立 happens-before。使用时须明确:保护哪些共享变量、是否使用同一把锁。需要可中断、超时、多条件队列时,应考虑 Lock(见 §6)。

5. 常见误区与最佳实践

误区正确做法
锁错对象(synchronized(new Object()) 每次新建)使用共享的 final 锁对象
在 synchronized 内做耗时 IO缩小临界区,只保护共享状态
混用 wait 与 sleepwait 释放锁,sleep 不释放
认为 synchronized 一定慢无竞争时 JVM 有锁消除/粗化等优化(见 §7.7)

java.util.concurrent 文档指出:高并发场景下,ConcurrentHashMap 通常优于 Collections.synchronizedMap;但若并发度不高,后者也足够简单可用。选型取决于竞争程度与 API 需求,而非「synchronized 已过时」。

6. 与 Lock / j.u.c 的对比

特性synchronizedReentrantLock
锁释放自动(块结束/方法返回)手动 unlock(),需 finally
公平性非公平可选公平/非公平
可中断lockInterruptibly()
超时tryLock(timeout)
条件队列单一(wait/notify)多个 Condition

选型建议:

  • 简单互斥、临界区短 → synchronized(代码简洁,JVM 优化充分)
  • 需要可中断、超时、公平锁、多条件等待 → ReentrantLock
  • Virtual Thread 场景下长时间阻塞 → 优先 ReentrantLock 或缩小 synchronized 范围(见 §8)

7. HotSpot 实现(JDK 8 源码)

7.1 对象头结构

HotSpot 中普通 Java 对象(oopDesc)头部至少包含两部分:

┌─────────────────┬─────────────────┐
│   Mark Word     │  Klass Pointer  │  ← 普通对象(64 位)
│   (8 bytes)     │   (8 bytes)     │
└─────────────────┴─────────────────┘
  • Mark Word:哈希码、GC 年龄、锁状态等;synchronized 的锁信息主要存储于此
  • Klass Pointer:指向类元数据(-XX:+UseCompressedOops 时为 4 字节 narrowKlass)
  • 数组对象在 Klass Pointer 之后还有 array length 字段

oopDesc 结构:

// oop.hpp
class oopDesc {
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  // ...
};

7.2 Mark Word 位布局与锁状态

markOop.hpp 注释描述了 64 位 JVM 下的布局:

正常对象(无锁):

unused:25 | hash:31 | unused:1 | age:4 | biased_lock:1 | lock:2

偏向锁:

JavaThread*:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2

lock:2 末两位 — 四种状态:

lock:2状态Mark Word 含义
01无锁 / 偏向锁常规 header 或偏向线程指针
00轻量级锁ptr 指向线程栈上 BasicLock
10重量级锁ptr 指向 ObjectMonitor
11GC 标记用于 markSweep,非常态

源码注释原文:

// markOop.hpp
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object

同一 Mark Word 在不同状态下复用各 bit 字段:hash(identity hashCode)、age(GC 年龄,最大 15)、biased_lock + epoch(偏向锁代际)。

7.3 锁升级路径(JDK 8)

flowchart LR
    noLock[无锁] --> biased[偏向锁]
    biased --> light[轻量级锁]
    light --> heavy[重量级锁]
    biased -->|"撤销 bias"| light
  • 偏向锁biasedLocking.hpp):同一线程反复加锁,Mark Word 记录线程 ID,无需 CAS
  • 轻量级锁:CAS 将 Mark Word 复制到线程栈 BasicLock,成功则栈上锁定
  • 重量级锁:竞争失败时 inflateObjectMonitor

锁只能升级,不能降级(偏向锁撤销除外)。JDK 15 起偏向锁逐步废弃(见 §8)。

7.4 加锁/解锁核心流程

synchronizer.cpp 是监视器 enter/exit 的核心实现:

fast_enter — 先处理偏向锁,再进入 slow_enter:

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 if (UseBiasedLocking) {
    if (!SafepointSynchronize::is_at_safepoint()) {
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    } else {
      BiasedLocking::revoke_at_safepoint(obj);
    }
  }
 slow_enter (obj, lock, THREAD) ;
}

slow_enter — CAS 抢轻量级锁,失败则 inflate:

void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  if (mark->is_neutral()) {
    lock->set_displaced_header(mark);
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      return;  // CAS 成功,轻量级锁
    }
  } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    lock->set_displaced_header(NULL);  // 可重入
    return;
  }
  lock->set_displaced_header(markOopDesc::unused_mark());
  ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}

fast_exit — CAS 恢复 Mark Word;失败走重量级 exit:

void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
  markOop dhw = lock->displaced_header();
  if (dhw == NULL) {
     return;  // 递归退出,无需操作
  }
  markOop mark = object->mark();
  if (mark == (markOop) lock) {
     if ((markOop) Atomic::cmpxchg_ptr(dhw, object->mark_addr(), mark) == mark) {
        return;  // 轻量级解锁成功
     }
  }
  ObjectSynchronizer::inflate(THREAD, object, inflate_cause_vm_internal)->exit(true, THREAD);
}

7.5 ObjectMonitor 与 wait/notify

轻量级锁竞争失败时,对象头膨胀为指向 ObjectMonitor 的指针。关键字段:

字段含义
_owner当前持有锁的线程
_recursions重入次数
_WaitSet调用 wait() 的线程队列
_EntryList阻塞等待锁的线程队列
_cxq竞争队列(Contention Queue)

wait 流程:释放锁 → 线程加入 _WaitSet → park 阻塞 → 被 notify 后移入 _EntryList → 重新竞争锁 → 从 wait() 返回。

notify 流程:从 _WaitSet 移出一个(或全部)线程到 _EntryList,unpark 唤醒。

7.6 膨胀(Inflate)触发场景

synchronizer.hpp 定义了膨胀原因:

typedef enum {
  inflate_cause_vm_internal = 0,
  inflate_cause_monitor_enter = 1,
  inflate_cause_wait = 2,
  inflate_cause_notify = 3,
  inflate_cause_hash_code = 4,
  inflate_cause_jni_enter = 5,
  inflate_cause_jni_exit = 6,
} InflateCause;

典型触发:多线程竞争同一把轻量级锁、wait/notify、计算 identity hashCode(可能与锁 bits 冲突)、JNI 监视器操作等。

7.7 JVM 锁优化策略

HotSpot 对 synchronized 的优化分两层:运行时锁升级(§7.3)与 编译器锁优化(C2 为主)。

flowchart TB
    subgraph compileTime [编译期 C2 优化]
        LE[锁消除]
        LC[锁粗化]
        NL[嵌套锁消除]
    end
    subgraph runtime [运行时优化]
        BL[偏向锁]
        LL[轻量级锁 + CAS]
        SP[自适应自旋]
        DF[Monitor deflation]
    end
    LE -->|"对象未逃逸"| noLockPath[无锁执行]
    LC -->|"合并相邻 lock/unlock"| fewerOps[减少加锁次数]
    NL -->|"同对象重复加锁"| outerOnly[只保留最外层]
    BL --> LL --> SP --> heavy[重量级锁 ObjectMonitor]
    heavy --> DF

(1)锁消除(Lock Elimination)

条件:C2 逃逸分析-XX:+DoEscapeAnalysis)判定锁对象未逃逸出当前线程,则 lock/unlock 对其他线程不可见,同步无必要。

实现ConnectionGraph::optimize_ideal_graph() 将锁标记为 NonEscObj,阻止 macro 展开,编译后去掉加解锁。

public String concat(String a, String b) {
    StringBuffer sb = new StringBuffer();  // sb 未逃逸
    sb.append(a);  // StringBuffer 内部 synchronized 可能被消除
    sb.append(b);
    return sb.toString();
}

callnode.cpp 中的判断逻辑:

if (can_reshape && EliminateLocks && !is_non_esc_obj()) {
  ConnectionGraph *cgr = phase->C->congraph();
  if (cgr != NULL && cgr->not_global_escape(obj_node())) {
    this->set_non_esc_obj();  // 标记消除
    return result;
  }
}

对象若逃逸(作为返回值、写入静态字段、传入未知方法),则不能消除。反优化(deoptimization)时可能需要 relock 恢复监视器状态。

(2)锁粗化(Lock Coarsening)

思路:相邻多次对同一对象加锁/解锁,合并为一次更大范围的锁。

// JIT 可能将下面两段粗化为一个 synchronized 块
synchronized (obj) { doA(); }
synchronized (obj) { doB(); }

实现:C2 AbstractLockNode::Ideal()find_matching_unlock() 将 lock-unlock-lock 序列标记为 Coarsened

HotSpot 参数 -XX:+EliminateLocks(默认开启)的描述正是 “Coarsen locks when possible”。粗化会扩大锁持有范围,JVM 仅在判定合并更划算时应用。

(3)嵌套锁消除(Nested Lock Elimination)

同一线程对同一对象的嵌套 synchronized,内层 lock/unlock 可被消除:

synchronized (obj) {
    synchronized (obj) {  // 内层可能被消除
        // ...
    }
}

-XX:+EliminateNestedLocks(默认开启)控制,LockNode::is_nested_lock_region() 检测嵌套区域。

(4)运行时:偏向锁 / 轻量级锁 / 自旋

策略作用关键参数
偏向锁单线程反复进入,Mark Word 记录线程 ID-XX:+UseBiasedLocking(JDK 8 默认)
轻量级锁无竞争时 CAS 抢栈锁自动
自适应自旋重量级锁竞争时 spin 再 parkObjectMonitorKnob_PreSpin

自旋发生在 inflate 为 ObjectMonitor 之后、线程 park 之前。

(5)Monitor 收缩(Deflation)

重量级锁膨胀后,若无竞争,在 SafepointObjectSynchronizer::deflate_idle_monitors() 回收 ObjectMonitor,Mark Word 恢复为无锁/轻量级状态,与 inflate 对称。

(6)优化策略对比

策略阶段默认主要 JVM 参数效果
锁消除C2 编译DoEscapeAnalysis + EliminateLocks去掉整个 lock/unlock
锁粗化C2 编译EliminateLocks减少中间 unlock/lock
嵌套锁消除C2 编译EliminateNestedLocks去掉内层 lock/unlock
偏向锁运行时开(JDK8)UseBiasedLocking减少 CAS
轻量级锁运行时避免 inflate
自适应自旋运行时减少 park/唤醒
Monitor 收缩Safepoint回收空闲 Monitor

这些优化对开发者透明。理解它们有助于解释「无竞争 synchronized 为何很快」,但不能依赖锁消除保证正确性——并发正确性仍须按 JMM 规则编写代码。

8. 现代 JDK 演进(与 JDK 8 对比)

阅读 JDK 8 源码时,需注意以下版本差异:

变化说明
偏向锁废弃JDK 15 默认关闭(-XX:-UseBiasedLocking),JDK 18 移除;现代路径为无锁 → 轻量级 → 重量级
锁优化持续C2 的 fast_lock 内联路径持续演进,但 Mark Word + ObjectMonitor 核心模型不变;锁消除/粗化/嵌套锁消除仍有效
Virtual Threads(JEP 444)synchronized 块内阻塞会导致 pinning——载体平台线程无法释放去执行其他虚拟线程。长时间阻塞建议改用 ReentrantLock,或尽量缩小 synchronized 范围
诊断工具JFR 的 JavaMonitorEnter 事件、async-profiler 的 lock profiling 等,可用于观察锁竞争与持有时间

Virtual Thread pinning 示例:

// 虚拟线程在 synchronized 内阻塞时,底层平台线程被"钉住"
Thread.startVirtualThread(() -> {
    synchronized (lock) {
        Thread.sleep(10_000);  // pinning:平台线程无法服务其他虚拟线程
    }
});

注意:该问题在 JDK 24 (JEP 491)中已经被正式修复,通过改变 HotSpot VM 对对象监视器(synchronized 锁)的实现,使得虚拟线程在 synchronized 块内阻塞时,也能够释放其载体线程(平台线程)。

对于 I/O 密集型、大量虚拟线程的场景,优先使用 ReentrantLock + lockInterruptibly,或基于 java.util.concurrent 的非阻塞结构。

9. 总结

理解 synchronized 的三层模型:

  1. 语法层ACC_SYNCHRONIZED(方法)与 monitorenter/monitorexit(代码块)
  2. 语义层:JMM 中通过监视器锁规则建立 happens-before,提供互斥、可见性、有序性
  3. 实现层:对象头 Mark Word 存储锁状态,无竞争时偏向/轻量级锁,竞争时膨胀为 ObjectMonitor;C2 编译器还可做锁消除、粗化、嵌套锁消除

实践建议:

  • 默认场景用 synchronized 足够简洁且性能良好
  • 高竞争场景考虑 j.u.c 细粒度工具或减小锁粒度
  • Virtual Thread 场景避免在 synchronized 内长时间阻塞
  • 不要为触发锁消除而刻意写代码——优化是 JIT 的事,正确性靠 JMM