synchronized的原理以及锁升级机制

核心目标: 保证同一时间,只有一个线程能执行被synchronized保护的代码块或方法。

实现原理的核心:每个Java对象都自带一把"锁"和一个"监视器"(Monitor)。

1. 锁的载体:对象头 (Object Header)

  • 想象: 每个Java对象(比如new Object())在内存中除了存你的数据,还有一个"对象头",就像它的身份证和门禁卡。

  • 关键部分 - Mark Word: 对象头里最重要的部分叫Mark Word。它很小(通常32位或64位),但非常灵活,用来存储:

    • 对象的哈希码 (HashCode)
    • 垃圾回收的分代年龄 (GC Age)
    • 锁状态标志位: 这是核心!它标记这个对象当前处于哪种锁状态(无锁、偏向锁、轻量级锁、重量级锁)。
    • 指向重量级锁的指针: 当锁升级到重量级锁时,这里存储指向一个真正"锁对象"(Monitor)的地址。
    • 偏向线程ID / 轻量级锁指针: 在偏向锁或轻量级锁状态下,存储相关线程或栈帧的信息。

2. 锁的守护者:Monitor (监视器锁)

  • 想象: 当锁升级到"重量级锁"状态时,JVM会为这个对象关联一个真正的Monitor对象(也叫管程或监视器锁)。你可以把它想象成一个特殊的小房间(临界区)的管理员

  • Monitor 的结构:

    • Owner (持有者): 记录当前哪个线程持有这把锁(正在房间里执行代码)。初始为null
    • EntryList (入口队列/竞争队列): 当线程A持有锁时,线程B、C也想来获取锁,但它们发现Owner是A(房间有人),于是它们就在这个队列里阻塞等待。这是一个互斥队列。
    • WaitSet (等待集合): 如果持有锁的线程A自己主动调用wait()方法释放锁并等待,那么线程A就会进入这个WaitSet集合中休眠等待 ,直到其他线程调用notify()/notifyAll()唤醒它。这是一个协作队列。

3. 锁的升级过程 (锁膨胀 - 优化关键!)

Java为了在不同竞争场景下平衡性能和开销,设计了锁升级机制:

  1. 无锁 (New / Normal):

    • 对象刚创建时,或者锁被释放后。
    • Mark Word里没有锁信息(或标志位是无锁)。
    • 多个线程可以自由竞争。
  2. 偏向锁 (Biased Locking):

    • 场景: 绝大多数情况下,锁总是被同一个线程多次访问(比如一个线程循环调用某个同步方法)。竞争极少。

    • 操作:

      • 当第一个线程Thread-A访问同步块时,JVM通过CAS操作 (Compare-And-Swap,一种CPU原子指令)尝试将Mark Word中的锁标志位设置为"偏向锁",并把自己的线程ID记录到Mark Word中。
      • 成功: Thread-A以后每次进入这个同步块,只需要检查Mark Word里的线程ID是不是自己。如果是,直接执行!开销极小(就一次内存地址比较),无需任何原子操作。
    • 优势: 没有竞争时,性能接近无锁!

    • 失效: 一旦有另一个线程Thread-B尝试获取锁(意味着有竞争了),偏向锁就会撤销。Thread-B会等待Thread-A到达全局安全点(如GC点)暂停,然后检查Thread-A是否还活着或已退出同步块。

      • 如果Thread-A已退出或不活动,撤销偏向锁,恢复到无锁状态,Thread-B可以尝试获取(可能直接升级为轻量级锁)。
      • 如果Thread-A还在活动,则升级为轻量级锁
  3. 轻量级锁 (Lightweight Lock / 自旋锁):

    • 场景: 存在少量、短时间的竞争。线程交替执行同步块,不太会长时间阻塞。

    • 操作:

      • 线程在进入同步块前,JVM会在当前线程的栈帧 中创建一个叫Lock Record的空间,用于保存锁对象原始的Mark Word拷贝 (Displaced Mark Word)。

      • 然后,线程使用CAS操作 尝试将锁对象的Mark Word更新为指向自己栈帧中Lock Record的指针。

      • 成功: 锁标志位变为"轻量级锁",线程获取锁成功。

      • 失败: 两种情况:

        • 锁重入: 如果发现锁对象的Mark Word指向的就是自己栈帧中的Lock Record,说明是同一个线程再次进入(可重入),直接执行。
        • 存在竞争: 其他线程占用了锁。此时,当前线程不会立即阻塞,而是采用自旋 策略(在CPU上空循环等待一小段时间),不断尝试CAS操作去获取锁。自旋避免了昂贵的线程阻塞和唤醒操作。
    • 优势: 在低竞争下,避免了OS层面的线程切换开销。

    • 升级: 如果自旋达到一定次数(JVM自适应策略决定)或者自旋期间又来了第三个线程竞争,轻量级锁就会升级为重量级锁

  4. 重量级锁 (Heavyweight Lock / 互斥锁):

    • 场景: 存在激烈、长时间的竞争。

    • 操作:

      • 锁标志位变为"重量级锁"。

      • Mark Word中存储指向Monitor对象(前面提到的那个管理员)的指针。

      • 未获取到锁的线程,不再自旋,而是被JVM通过操作系统内核调用(如pthread_mutex_lock挂起(阻塞) ,放入MonitorEntryList队列中等待。

      • 当持有锁的线程退出同步块,释放锁时:

        • 它会唤醒EntryList队列中的一个或所有线程(取决于策略)。
        • 被唤醒的线程会重新竞争锁(可能又失败,继续阻塞)。
    • 开销: 涉及用户态到内核态的切换,线程阻塞和唤醒代价非常高昂

    • 优势: 在激烈竞争下,避免了CPU空转(自旋)浪费资源,让出CPU给其他线程用。

4. synchronized在代码中的表现

  • 同步代码块 (synchronized(obj) {...}):

    • 锁对象就是你显式指定的obj
    • JVM编译后在代码块开始处插入monitorenter指令,在结束处和异常路径处插入monitorexit指令。
    • 执行monitorenter指令时,JVM会根据锁对象的状态(无锁/偏向/轻量级/重量级)执行上面描述的相应锁获取/升级逻辑。
    • 执行monitorexit指令时,执行锁释放逻辑(释放锁、唤醒等待线程、可能降级锁状态)。
  • 同步实例方法 (public synchronized void method()):

    • 锁对象就是该方法所属的当前对象实例 (this)
    • 方法调用时,JVM会检查方法的访问标志ACC_SYNCHRONIZED。有这个标志的方法,其调用和返回在JVM内部会被隐式地加上monitorentermonitorexit的效果,锁对象就是this
  • 同步静态方法 (public static synchronized void method()):

    • 锁对象是该方法所属类的Class对象 (如MyClass.class)。
    • 同样通过ACC_SYNCHRONIZED标志实现,锁对象是当前类的Class对象。

5. 关键特性与要点

  1. 可重入性 (Reentrancy): 同一个线程可以多次 获取同一个锁对象上的锁。例如,在一个synchronized方法里调用另一个synchronized方法(锁对象相同)是允许的。JVM通过记录锁的持有计数来实现。这是synchronized的基础特性,非常重要。
  2. 自动释放: 无论同步块是正常退出还是因为异常退出,JVM都会保证锁被释放。这是由编译器插入的monitorexit指令(包括在异常处理表里)保证的。
  3. 内存可见性: 获得锁不仅意味着独占执行权,还意味着线程会刷新工作内存并从主内存重新加载共享变量 ,保证在锁保护区内看到的变量是最新的。释放锁时,会把修改刷新回主内存。这解决了线程间的可见性问题。
  4. 锁的是对象,不是代码: 一定要牢记,锁是绑定在对象上的。两个线程锁不同的对象,不会互相阻塞。锁同一个对象才会互斥。

总结流程图

text

rust 复制代码
线程尝试进入synchronized块
      |
      V
检查对象锁状态 (Mark Word)
      |
      |--- 无锁? ---> CAS尝试获取偏向锁(记录线程ID) ---成功---> 执行代码 (下次进来只需检查ID)
      |             |
      |             ---失败(竞争)---> 升级轻量级锁
      |
      |--- 偏向锁? ---> 检查线程ID是否是自己?
      |             |
      |             |--- 是 ---> 执行代码 (重入计数+1)
      |             |
      |             --- 否 ---> 撤销偏向锁 (暂停原线程) ---> 升级轻量级锁
      |
      |--- 轻量级锁? ---> 检查锁记录指针是否指向自己栈帧?
      |                |
      |                |--- 是 (重入) ---> 执行代码 (重入计数+1)
      |                |
      |                --- 否 ---> CAS尝试将Mark Word指向自己的锁记录
      |                           |
      |                           |--- 成功 ---> 执行代码
      |                           |
      |                           --- 失败 ---> 自旋等待 (循环尝试CAS)
      |                                       |
      |                                       |--- 成功 ---> 执行代码
      |                                       |
      |                                       --- 自旋超限或有新竞争者 ---> 升级重量级锁
      |
      |--- 重量级锁? ---> 检查Monitor的Owner是否是自己?
                     |
                     |--- 是 (重入) ---> 执行代码 (重入计数+1)
                     |
                     --- 否 ---> 进入EntryList队列阻塞等待 (OS挂起线程)
                                 |
                                 |--- 被唤醒 ---> 竞争锁 (可能失败,继续阻塞)
                                 |
                                 --- 竞争成功 ---> 成为Owner,执行代码

退出synchronized块:
  重入计数-1
  如果计数 > 0: 直接返回 (锁还没完全释放)
  如果计数 = 0:
      轻量级锁: CAS将Displaced Mark Word拷贝回对象头 (恢复状态)
      重量级锁: 设置Owner为null,唤醒EntryList中的一个/所有等待线程
      无操作 (偏向锁在撤销时已处理)

简单记忆:

  • 偏向锁: 贴个名字标签(线程ID),熟人(同一线程)来了直接进。
  • 轻量级锁: 门口挂个小牌子(指向线程栈的指针),大家(少量线程)在门口稍微等一下(自旋),谁先抢到牌子(CAS成功)谁进。
  • 重量级锁: 门口排长队(EntryList),有管理员(Monitor),里面的人出来管理员才叫下一个进去(线程阻塞唤醒)。

理解了这个锁升级过程和Monitor的概念,就掌握了synchronized的核心原理。它通过这种分层优化的策略,在无竞争、低竞争和高竞争的不同场景下,都尽可能达到最好的性能。

相关推荐
洛卡卡了2 分钟前
“改个配置还要发版?”搞个配置后台不好吗
前端·后端·架构
林太白13 分钟前
CommonJS和ES Modules篇
前端·面试
用户753897552817522 分钟前
《手写解释器》第4章 扫描
后端
Running_C23 分钟前
HTTP 断点续传与大文件上传,现在面试必问吧
前端·面试
kakaZhou71938 分钟前
日志系统之Grafana Loki
后端·开源
菜菜的后端私房菜41 分钟前
Protocol Buffers!高效数据通信协议
java·后端·protobuf
晴殇i41 分钟前
前端视角下的单点登录(SSO)从原理到实战
前端·面试·trae
树獭叔叔1 小时前
Python 锁机制详解:从原理到实践
后端·python
用户15186530413841 小时前
从传统办公软件到云协作Flash Table AI分钟级生成表单,打造企业远程高效率办公的利器
前端·后端·前端框架
寻月隐君1 小时前
Rust 核心设计:孤儿规则与代码一致性解析
后端·rust·github