参考小林codingHome

JavaGuideJava 面试指南

Synchronized原理

首先要介绍一下Object对象锁。在Java中,每个实例都有一个关联的Monitor锁。当线程进入synchronized代码块时,会尝试获取Object对应的Monitor。

使用monitorenter指向同步代码块开始的位置,monitorexit指向同步代码块结束的位置

两个monitorexit,一个对应正常退出,一个对应异常退出。

JVM执行monitorenter指令时,会尝试获取对象的监视器锁。如果锁是空闲的,那么线程获取锁,继续执行。如果锁已被其他线程占有,那么线程阻塞,等待锁释放。

monitor底层基于操作系统的互斥量来实现

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。

构造方法是否可以用synchronized修饰

构造方法不能使用 synchronized 关键字修饰。不过,可以在构造方法内部使用 synchronized 代码块。

另外,构造方法本身是线程安全的,但如果在构造方法中涉及到共享资源的操作,就需要采取适当的同步措施来保证整个构造过程的线程安全。

synchronized锁实例和锁类的class对象

参考Java synchronized 机制详解:实例锁与类锁,普通方法锁与静态方法锁的区别 - Astrylia小站

synchronized是可重入锁

synchronized 是一种可重入锁,意思是同一个线程在持有某个对象的锁之后,可以再次获取该对象的锁而不会被阻塞。在 JVM 中,每个对象都关联一个 Monitor 监视器。当一个线程第一次获取锁时,JVM 会将该线程记录为持有者,并将重入计数器设置为 1。 如果该线程再次进入同一个 synchronized 方法或代码块,JVM 会检测到它已经是锁的持有者,于是允许重入,并将计数器加 1。 当该线程退出同步块时,计数器减 1,只有当计数器为 0 时,锁才会真正释放,其他线程才有机会获取这个锁。

synchronized锁升级

1. 无锁(No Lock)

对象刚创建时,没有被加锁。


2. 偏向锁(Biased Lock)

目的:优化 单线程重复加锁 的情况

  • 对象第一次被某个线程获取后,JVM 在对象头(Mark Word)中记录这个线程的 ID
  • 同一个线程再次进入同步块时,只需比对线程 ID 是否一致,就可以直接进入,无需 CAS(高性能)。
  • 如果其他线程尝试获取锁 → 偏向锁会被撤销(撤销过程需要安全点、可能有 STW),进入下一阶段。

3. 轻量级锁(Lightweight Lock)

目的:优化 短时间内多线程交替加锁但无竞争 的情况

  • 当第二个线程尝试获取偏向锁时,JVM 会尝试使用 CAS 机制尝试加锁
  • 若 CAS 成功,就会变成轻量级锁。
  • 在轻量级锁下,如果没有竞争,线程依然可以非常快地获取和释放锁。

如果 CAS 失败,说明出现了真实竞争,进入下一阶段。


4. 重量级锁(Heavyweight Lock)

目的:处理多个线程同时竞争锁的场景

  • JVM 将该对象的锁升级为重量级锁,涉及 Monitor(监视器)。
  • 被阻塞的线程会挂起(进入等待队列),等待锁释放。
  • 锁的获取与释放需要线程阻塞/唤醒,这是一种昂贵的系统调用(涉及上下文切换)。

锁的升级是单向的、不可降级的!

一旦进入更高级别的锁状态,不会降级为偏向锁或轻量级锁。

synchronized锁优化

  • 锁膨胀:synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程
  • 锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。