真实面经题目 · 原创解析
synchronized底层实现的原理?
synchronized 是 JVM 级别的内置同步机制。它通过 monitorenter/monitorexit 或方法访问标志进入对象监视器,基于对象头 Mark Word 记录锁状态,并在不同竞争程度下使用轻量级锁、重量级锁等实现互斥。它支持可重入,退出同步块会建立 happens-before 关系,也与 wait/notify 的条件等待机制绑定。
真实面经题目 · 原创解析
synchronized 是 JVM 级别的内置同步机制。它通过 monitorenter/monitorexit 或方法访问标志进入对象监视器,基于对象头 Mark Word 记录锁状态,并在不同竞争程度下使用轻量级锁、重量级锁等实现互斥。它支持可重入,退出同步块会建立 happens-before 关系,也与 wait/notify 的条件等待机制绑定。
可以从字节码、对象结构、锁状态和内存语义四层回答。同步代码块会编译成 monitorenter 和 monitorexit 指令,线程进入时尝试获取对象关联的监视器,退出时释放监视器;同步方法不会显式出现这两个指令,而是通过方法的 ACC_SYNCHRONIZED 标志让 JVM 在方法调用和返回时自动加锁解锁。锁对象本身在对象头中有 Mark Word,里面会记录哈希、GC 年龄、锁标志位、线程或锁记录指针等信息。无竞争时 JVM 会尽量用轻量级方式完成加锁;出现竞争后会膨胀为重量级锁,线程阻塞和唤醒依赖操作系统调度。历史上还有偏向锁,但 JDK 15 起已废弃并默认关闭,现代 JDK 不应把偏向锁作为主线。synchronized 是可重入的,同一线程重复进入同一把锁会增加重入计数,退出到计数归零才真正释放。内存语义上,对一个锁的解锁 happens-before 后续线程对同一个锁的加锁,因此能保证同步块内写入对后续持锁线程可见。wait/notify/notifyAll 也依赖对象监视器,调用前必须持有对应锁,wait 会释放锁并进入等待队列,被通知后还要重新竞争锁。和 ReentrantLock 相比,synchronized 语法简单、异常安全、由 JVM 优化;ReentrantLock 提供可中断、公平锁、限时尝试加锁、多个 Condition 等更灵活能力,但需要手动释放锁。
同步代码块会在字节码中生成 monitorenter 和 monitorexit。monitorenter 表示进入某个对象的监视器,monitorexit 表示退出并释放。编译器通常会为正常路径和异常路径都生成释放逻辑,保证同步块中抛异常时锁也能被释放。同步方法则不同,字节码中通常看不到 monitorenter/monitorexit,而是方法表上带 ACC_SYNCHRONIZED 标志,由 JVM 隐式获取和释放锁。
每个 Java 对象都可以作为锁,逻辑上都可以关联一个对象监视器。线程进入 synchronized 时,竞争的是锁对象关联的监视器。监视器内部维护持有者、重入计数、竞争队列、等待队列等状态。一个线程持有锁后,其他线程再进入同一把锁会进入竞争流程;如果执行 wait,则线程会释放当前监视器并进入等待队列。
HotSpot 对象头中的 Mark Word 是理解 synchronized 的关键。Mark Word 会随对象状态变化而复用存储不同信息,例如对象哈希、分代年龄、锁标志位、指向栈中 Lock Record 的指针、指向重量级监视器的指针等。加锁不是给对象额外挂一个布尔值,而是 JVM 通过 Mark Word 和运行时数据结构协作表达锁状态。
轻量级锁主要服务于低竞争场景。线程进入同步块时,会在自己的栈帧中创建锁记录,并尝试通过 CAS 把对象 Mark Word 替换为指向该锁记录的指针。如果 CAS 成功,说明当前线程获得锁;如果发现锁已经由当前线程持有,则走可重入逻辑;如果 CAS 失败并存在真实竞争,JVM 会根据情况自旋或进入锁膨胀。
当多个线程对同一把锁发生明显竞争,轻量级方案无法低成本解决时,锁会膨胀为重量级锁。此时 Mark Word 会指向重量级监视器,竞争失败的线程可能被阻塞,后续唤醒依赖操作系统调度。重量级锁成本更高,因为涉及用户态和内核态切换、线程挂起与恢复,但在高竞争下比长时间自旋更合适。
偏向锁是较早 HotSpot 中针对同一线程反复进入同一把锁的优化,思想是把锁偏向某个线程,后续该线程进入时减少 CAS 成本。但这不是现代 synchronized 的主线。JDK 15 起偏向锁已被废弃并默认关闭,因此回答时可以把它作为历史优化补充说明,但不要把当前实现讲成主要依赖偏向锁。
synchronized 是可重入锁。同一线程已经持有某对象锁时,再次进入同一对象的 synchronized 代码不会死锁,而是增加重入层数或记录重入状态。只有该线程执行完所有嵌套同步区域,释放次数与进入次数匹配后,锁才真正对其他线程可用。
synchronized 不只保证互斥,还保证可见性和有序性。Java 内存模型规定,对同一把锁的一次解锁 happens-before 后续线程对这把锁的一次加锁。也就是说,线程 A 在同步块内修改的共享变量,在 A 释放锁后,线程 B 再获得同一把锁时应当可见。
因为 wait 操作的是对象监视器的等待队列。线程必须先持有这个监视器,才能原子地释放锁并进入等待队列,否则会破坏等待和通知的同步关系,所以 JVM 会抛出 IllegalMonitorStateException。
volatile 主要保证可见性和禁止特定重排序,不保证复合操作的互斥原子性;synchronized 同时保证互斥、可见性和有序性,适合保护多个变量或复合状态更新。
轻量级锁主要依赖 CAS 和线程栈中的锁记录,失败成本较低;重量级锁需要监视器对象、阻塞队列、线程挂起和唤醒,可能触发操作系统调度,因此上下文切换成本更高。
不一定。现代 JVM 对 synchronized 有大量优化,在普通互斥场景下性能通常足够好。ReentrantLock 的优势主要是能力更丰富,而不是绝对更快。
实例同步方法锁 this,静态同步方法锁当前类的 Class 对象。同步代码块锁的是括号里表达式得到的对象。锁对象不同,互斥关系就不同。