资讯动态

Java synchronized锁机制原理与性能优化实践

发布时间:2026/10/26 11:17:08 来源:尧图企业网站定制
1. 从Java线程同步的痛点说起第一次在多线程环境下操作共享变量时我遇到了经典的银行转账问题两个线程同时给同一个账户存款结果余额竟然比预期少了一部分。这个经历让我意识到在并发编程中仅仅依靠代码逻辑的正确性是不够的还需要考虑线程间的执行顺序问题。而synchronized关键字就是Java为解决这类问题提供的最基础也最重要的同步机制。2. synchronized的底层实现原理2.1 对象头与Mark Word每个Java对象在内存中存储时都包含对象头信息。对象头由两部分组成Mark Word和类型指针。Mark Word是实现synchronized锁的关键所在它记录了对象的锁状态信息。在32位JVM中Mark Word的结构如下锁状态25bit4bit1bit(偏向锁)2bit(锁标志)无锁对象的hashCode对象分代年龄001偏向锁线程IDEpoch101轻量级锁指向栈中锁记录的指针00重量级锁指向互斥量(Monitor)的指针10GC标记空112.2 Monitor机制当使用synchronized修饰方法或代码块时JVM会在编译后的字节码中插入monitorenter和monitorexit指令。这两个指令分别对应获取锁和释放锁的操作。每个对象都与一个Monitor相关联Monitor包含以下关键字段_owner记录持有该Monitor的线程_EntryList存放等待获取锁的线程_WaitSet存放调用了wait()方法的线程注意Monitor的获取是互斥的同一时刻只有一个线程能够持有Monitor。其他线程必须等待当前线程释放锁后才能尝试获取。3. 锁升级的全过程解析3.1 无锁状态新创建的对象默认处于无锁状态。此时Mark Word中存储的是对象的hashCode、分代年龄等信息。锁标志位为01偏向锁标志为0。3.2 偏向锁阶段当第一个线程访问同步块时JVM会将对象头中的线程ID设置为当前线程ID并将偏向锁标志置为1。此时对象进入偏向锁状态。偏向锁的核心思想是如果只有一个线程访问同步块就不需要进行实际的同步操作。偏向锁的获取过程检查Mark Word中的线程ID是否指向当前线程如果是直接执行同步代码如果不是检查偏向锁标志是否为1如果为1尝试通过CAS操作将线程ID替换为当前线程ID如果替换成功执行同步代码如果替换失败说明存在竞争开始撤销偏向锁3.3 轻量级锁阶段当第二个线程尝试获取锁时偏向锁就会升级为轻量级锁。轻量级锁的实现依赖于线程栈中的锁记录(Lock Record)和CAS操作。轻量级锁的获取过程在当前线程的栈帧中创建锁记录空间将对象头中的Mark Word复制到锁记录中(Displaced Mark Word)尝试通过CAS将对象头中的Mark Word替换为指向锁记录的指针如果替换成功当前线程获得锁如果替换失败检查对象头中的Mark Word是否指向当前线程的栈帧如果是直接执行同步代码如果不是说明存在竞争锁会膨胀为重量级锁3.4 重量级锁阶段当多个线程竞争同一个锁时轻量级锁就会膨胀为重量级锁。重量级锁依赖于操作系统的互斥量(Mutex)实现线程的阻塞和唤醒需要从用户态切换到内核态开销较大。重量级锁的特点通过Monitor对象实现未获取锁的线程会被阻塞放入_EntryList线程的阻塞和唤醒需要操作系统介入适用于高并发场景4. 锁升级的触发条件与性能影响4.1 各阶段锁的适用场景锁类型适用场景优点缺点偏向锁只有一个线程访问同步块几乎没有同步开销存在锁撤销的开销轻量级锁多个线程交替执行没有真正竞争避免线程阻塞自旋会消耗CPU重量级锁多个线程同时竞争锁避免CPU空转线程阻塞唤醒开销大4.2 锁升级的触发条件偏向锁升级为轻量级锁当第二个线程尝试获取锁时调用hashCode()方法(因为偏向锁的Mark Word中没有空间存储hashCode)轻量级锁升级为重量级锁自旋超过一定次数(默认10次)等待线程数超过CPU核心数的一半4.3 锁降级在特定情况下JVM也会进行锁降级操作。例如在GC时为了减少STW时间可能会将重量级锁降级为轻量级锁。但锁降级不是JVM的常规行为开发者不应依赖这种行为。5. 实战中的锁优化建议5.1 减少锁的持有时间将不需要同步的代码移出同步块尽量减少临界区的大小。例如// 不推荐的写法 public synchronized void process() { // 准备数据(不需要同步) prepareData(); // 处理数据(需要同步) handleData(); } // 推荐的写法 public void process() { // 准备数据(不需要同步) prepareData(); synchronized(this) { // 处理数据(需要同步) handleData(); } }5.2 减小锁的粒度将一个大的锁拆分为多个小的锁减少锁的竞争。例如ConcurrentHashMap就采用了分段锁的设计。5.3 避免锁的滥用不是所有共享数据都需要加锁。对于读多写少的场景可以考虑使用读写锁(ReentrantReadWriteLock)或乐观锁(CAS)。5.4 锁粗化在循环内部加锁会导致频繁的锁获取和释放此时JVM可能会进行锁粗化优化将锁移到循环外部。但开发者不应依赖JVM的优化最好在编码时就考虑这一点。6. 常见问题排查与性能调优6.1 如何判断锁竞争激烈使用JVisualVM或JConsole查看线程状态检查BLOCKED状态的线程数量使用JStack查看线程堆栈分析锁的持有情况6.2 锁竞争导致的性能问题症状CPU使用率不高但吞吐量低大量线程处于BLOCKED状态应用响应时间变长解决方案减小锁粒度使用读写锁替代独占锁考虑使用无锁数据结构(如ConcurrentHashMap)缩短临界区代码6.3 死锁问题排查死锁的四个必要条件互斥条件请求与保持条件不剥夺条件循环等待条件排查方法使用jstack -l 获取线程转储查找deadlock关键词分析线程等待的锁和持有的锁7. JVM参数调优建议7.1 偏向锁相关参数-XX:UseBiasedLocking启用偏向锁(JDK15后已废弃)-XX:BiasedLockingStartupDelay0JVM启动后立即启用偏向锁7.2 自旋锁相关参数-XX:UseSpinning启用自旋锁(JDK6后默认开启)-XX:PreBlockSpin10自旋次数默认值7.3 锁膨胀相关参数-XX:DoEscapeAnalysis启用逃逸分析(有助于锁消除)-XX:EliminateLocks启用锁消除优化8. 从synchronized看Java并发演进synchronized的优化历程反映了Java并发编程的发展趋势JDK1.6之前只有重量级锁性能较差JDK1.6引入偏向锁和轻量级锁大幅提升单线程和低竞争场景性能JDK后续版本不断优化锁的实现减少内存开销和上下文切换在实际开发中理解这些底层原理能帮助我们更准确地诊断并发问题编写更高效的并发代码做出更合理的架构设计决策

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑