Synchronized 与 Lock 的实现原理及对比 目录Synchronized 与 Lock 的实现上的区别Lock 的实现原理API 层面什么情况下只能使用 Lock效率上的区别重量级锁可以降级吗轻量级锁触发条件及 inflate膨胀过程Synchronized 与 Lock 的实现上的区别synchronized的底层是完全基于 JVM 虚拟机的实现开发者无法看见具体的 Java 实现源码。从字节码上如果是同步块的情况下依赖于monitorenter和monitorexit指令。同步代码块当执行到monitorenter时线程试图获取对象的锁即 Monitor执行完或发生异常时执行monitorexit释放锁。同步方法并没有这两个指令而是在方法的全局标志flags中设置了ACC_SYNCHRONIZED标志。JVM 激活方法时会检查该标志从而隐式地调用 Monitor。Lock 的实现原理API 层面Lock如ReentrantLock完全是由 Java 代码编写的它的核心是依托于 AQSAbstractQueuedSynchronizer抽象队列同步器框架。AQS 核心机制AQS 的底层实现主要依靠三个核心要素volatile 状态位state内部有一个volatile int state变量用来表示锁的状态如 0 表示未锁1 表示已锁大于 1 表示重入次数。CASCompare And Swap操作线程尝试获取锁时通过底层Unsafe类的 CAS 操作原子性操作去修改state的值。如果修改成功说明获取锁成功。CLH 队列双向链表如果 CAS 修改失败说明锁被其他线程占用。当前线程会被封装成一个Node节点加入到 AQS 的双向同步队列中并通过LockSupport.park()挂起自己等待被唤醒。什么情况下只能使用 Lock需要“超时放弃”或“非阻塞尝试”获取锁时需要线程在等待锁时能够“响应中断”需要实现“公平锁”需要多个等待条件精准唤醒效率上的区别在传统的操作系统线程模型下两者的效率取决于竞争的激烈程度 低竞争/无竞争场景synchronized 略胜一筹或持平原理JVM 对synchronized做了大量的底层优化。如果一个锁没有竞争JVM 会使用轻量级锁通过底层 CAS 修改对象头甚至通过锁消除Lock Elimination优化掉不必要的锁。结果此时synchronized的性能开销几乎可以忽略不计甚至优于Lock的显式加锁。 中等竞争场景两者性能基本持平原理当出现一定程度的竞争时synchronized会升级为轻量级锁并进行自适应自旋Adaptive Spinning而Lock也会通过 AQS 进行 CAS 自旋尝试获取锁。结果两者的底层都是基于 CPU 的 CAS 原子指令性能差异微乎其微。 高竞争场景Lock 表现更稳定、上限更高原理当大量线程疯狂争抢同一个锁时synchronized会彻底升级为重量级锁引起的内核态/用户态切换和线程上下文切换开销会变大。而Lock如ReentrantLock由于其 AQS 内部拥有更精细的等待队列控制、非公平锁的抢占机制以及支持tryLock()这种“打得过就打打不过就跑”的策略能够有效避免线程大面积阻塞带来的系统雪崩。结果在极端高并发下Lock的吞吐量通常比synchronized更平稳。重量级锁可以降级吗在线程运行期加锁过程中不能降级。一旦锁升级为重量级锁当前争抢锁的线程绝不会在执行期间将其降级回轻量级锁或偏向锁。在锁闲置期无线程竞争时可以降级。JVM 底层有一套完善的回收机制当一个重量级锁长时间没有线程竞争、处于闲置状态时JVM 会收回其关联的 Monitor 对象并把锁对象恢复为无锁状态。轻量级锁触发条件及 inflate膨胀过程1. 触发膨胀的场景场景 A高并发下的多线程“正面撞车”CAS 失败过程线程 A 当前持有某个对象的轻量级锁对象头指向线程 A 的栈帧。此时线程 B 也企图获取这个锁它会尝试通过 CAS 操作将对象头指向自己的 Lock Record。结果线程 B 的 CAS 必定失败。因为此时对象头已经被线程 A 改变了。一旦 CAS 失败就意味着存在锁竞争轻量级锁就会开始向重量级锁膨胀。场景 B自适应自旋Adaptive Spinning达到极限在早期的 JVM 中CAS 失败后线程会固定自旋比如 10 次如果还没拿到锁就膨胀。现代 JVM 引入了自适应自旋原理JVM 会根据“上一次在同一个锁上自旋是否成功”以及“锁拥有者的状态”来决定自旋的次数。膨胀时机如果线程 B 在自旋期间线程 A 释放了锁那么线程 B 就能顺利升级为轻量级锁所有者。但如果线程 B 自旋了足够多的次数JVM 认为再旋下去就是纯纯浪费 CPU 算力了线程 A 依然没有释放锁线程 B 就会停止自旋开始触发锁膨胀。场景 C持有轻量级锁的线程调用了Object.wait()原理wait()、notify()和notifyAll()机制在底层是完全依赖ObjectMonitor管程中的等待队列_WaitSet来实现的。膨胀时机如果线程 A 已经拿到了轻量级锁但在同步块里调用了lockObj.wait()此时轻量级锁必须立刻膨胀为重量级锁。因为轻量级锁根本没有地方去存放被挂起的等待线程队列必须借助ObjectMonitor的结构。2. 锁膨胀的底层落地过程Inflate当 JVM 决定将锁膨胀为重量级锁时底层C 层面会经历一段非常严谨的“变身”流转发起膨胀通常是竞争失败的线程 B 触发线程 B 发现自旋失败调用 JVM 底层的ObjectSynchronizer::inflate方法。创建重量级锁实体线程 B 在 Native Memory堆外内存中申请分配一个ObjectMonitor对象。设置 Monitor 内部状态将ObjectMonitor的_owner指向当前正持有轻量级锁的线程 A。将原轻量级锁保存在线程 A 栈帧中的Displaced Mark Word备份完整复制到ObjectMonitor的_header属性中确保对象的 HashCode 和分代年龄不丢失。修改对象头关键的原子操作线程 B 通过 CAS 操作将 Java 对象的 Mark Word 改为指向该 ObjectMonitor 的指针 | 锁标志位 10重量级锁标志。阻塞自己线程 B 将自己封装成一个节点放入ObjectMonitor的_cxq或_EntryList阻塞队列中然后调用操作系统的底层指令如 Linux 的pthread_mutex_lock或park将自己挂起变为BLOCKED状态静静等待被唤醒。醒。