
2.3.1 类锁和对象锁面试官问这个主要是考察你对锁作用域和内存模型的敏感度。对象锁实例锁用法修饰普通实例方法或者synchronized(this)。本质锁的是当前对象的实例Object。场景不同实例对象之间的锁互不干扰。类锁全局锁用法修饰static静态方法或者synchronized(MyClass.class)。本质锁的是当前类的Class 对象在 JVM 中全局唯一。场景锁住整个类无论 new 了多少个实例都共用这一把锁。面试连环问“如果一个类里有一个synchronized静态方法和一个synchronized实例方法两个线程能同时执行吗”答能。因为它们锁的根本不是同一个对象一个是 Class 对象一个是实例对象。2.3.2 synchronized 的优化JVM 的极限微操早期synchronized是一把“重量级锁”直接调用 OS 的互斥量Mutex涉及用户态和内核态的切换极其耗时。JDK 1.6 引入了“锁膨胀”和“锁消除”等机制。2.3.2.1 锁消除 (Lock Elimination)原理JIT 编译器在编译时通过逃逸分析Escape Analysis发现某段代码中的锁对象不可能被多个线程共享即没有逃逸出当前线程。面试案例在方法内部new了一个StringBuffer并不断append。StringBuffer内部是用synchronized修饰的但因为它只是局部变量JIT 会直接把它的锁干掉直接执行原生的非安全方法极大提升性能。2.3.2.2 锁膨胀/粗化 (Lock Coarsening)原理如果 JVM 发现有一连串连续的、对同一个对象的“加锁-解锁”操作比如在for循环里synchronized频繁加解锁同样耗时。JVM 会将锁的范围扩大到整个循环外部。面试案例javafor (int i 0; i 100; i) { synchronized(lock) { /* 做点小事 */ } } // JVM 优化后 synchronized(lock) { for (int i 0; i 100; i) { /* 做点小事 */ } }2.3.2.3 锁升级 (Lock Escalation) —— 必考核心为了让锁的代价与竞争激烈程度相匹配锁的状态只能升不能降无锁对象刚创建。偏向锁只有一个线程访问。Mark Word 记录该线程 ID。后续该线程再次进入比对 ID 即可零开销注意由于维护成本高JDK 15 后默认废弃。轻量级锁出现多线程交替获取。利用CAS自旋获取锁不进入内核态阻塞线程。适合执行极快的同步块。重量级锁竞争激烈自旋超过一定次数或线程数太多。向操作系统申请互斥量Mutex没抢到锁的线程进入内核态阻塞性能最差但不耗 CPU。2.3.3 synchronized 的实现原理看懂字节码与对象头2.3.3.1 MarkWord对象头里的变色龙这是理解锁升级的物理载体。Java 对象头Object Header中的 Mark Word 在 64 位 JVM 下占 64 bit它为了节省内存复用了这块空间根据锁的状态记录不同的信息无锁记录对象的 HashCode、分代年龄。锁标志位01。偏向锁记录持有锁的线程 ID、Epoch。偏向标志位1锁标志位01。轻量级锁记录指向线程栈中 Lock Record 的指针。锁标志位00。重量级锁记录指向底层ObjectMonitor对象的指针。锁标志位10。(面试加分项说明synchronized修饰代码块时底层字节码使用monitorenter和monitorexit指令修饰方法时底层使用ACC_SYNCHRONIZED标志位。)2.3.3.2 查看锁对象 MarkWord实战工具面试官可能会问“你平时怎么证明锁升级了”答引入JOL (Java Object Layout)库。代码中调用ClassLayout.parseInstance(obj).toPrintable()。通过打印出来的二进制可以直观地看到锁标志位从01变00再变10的全过程。2.3.4 查看 ObjectMonitor重量级锁的真面目当锁升级到重量级锁时Mark Word 里存的指针指向的就是 C 底层的一个复杂结构体ObjectMonitor位于 HotSpot 源码objectMonitor.hpp。它的核心内部结构面试高频_owner指向当前持有锁的线程。_cxq竞争队列Contention Queue单链表。新来的、抢锁失败的线程会被放入这里。_EntryList候选队列存放从_cxq转移过来、准备竞争锁的线程。_WaitSet等待集合存放调用了Object.wait()方法被挂起的线程。底层执行流程线程执行monitorenter尝试把_owner设置为自身。如果_owner为空获取锁成功。如果不为空该线程会被封装成ObjectWaiter节点放入_cxq队列。底层通过调用操作系统的park()方法本质是 Linux 的futex系统调用将该线程挂起进入内核态阻塞。持有锁的线程执行完毕后释放锁并唤醒_cxq或_EntryList里的线程去抢锁。 面试闭环将知识点串联如果面试官问“聊聊synchronized的优缺点”高级回答“早期的synchronized因为直接调用操作系统的互斥量导致频繁的用户态与内核态切换性能很差。但自从 JDK 1.6 引入了锁升级机制后它变得更加智能化只有一个线程时用偏向锁无开销交替执行时用轻量级锁CAS 自旋用户态只有竞争极其激烈时才升级为重量级锁调用 ObjectMonitor进入内核态阻塞。再加上 JIT 编译器的锁消除和锁粗化优化现在的synchronized性能已经非常优异在大多数简单的互斥场景下甚至比ReentrantLock更好因为它是 JVM 原生支持的不需要手动释放锁极大降低了死锁风险。”