ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

深入解析Java synchronized锁机制与优化实践

深入解析Java synchronized锁机制与优化实践 1. 从synchronized的日常使用说起第一次接触synchronized是在处理用户积分并发修改的场景。当时系统频繁出现积分错乱加上这个关键字后问题立即消失那种药到病除的快感至今难忘。但真正理解它却是在某次面试被连续追问锁升级过程、偏向锁撤销等细节时。synchronized作为Java最基础的同步机制每个Java开发者都自称会用但能说清这些问题的不足三成为什么方法签名和代码块两种写法效果不同对象头里的Mark Word如何存储锁状态轻量级锁到底轻在哪里为什么高版本JDK默认开启偏向锁延迟这些正是大厂面试官最爱的连环追问点。本文将用实际代码演示内存布局分析的方式带你看透这个最熟悉的陌生人。2. synchronized的两种基础用法2.1 方法声明式同步在方法签名添加synchronized是最简单的用法public synchronized void transfer(Account target, int amount) { this.balance - amount; target.balance amount; }这种写法等价于用this对象作为锁的同步块public void transfer(Account target, int amount) { synchronized(this) { this.balance - amount; target.balance amount; } }关键区别方法声明式会体现在字节码的方法访问标志(ACC_SYNCHRONIZED)而代码块式会生成monitorenter/monitorexit指令2.2 对象锁与类锁的特殊性当synchronized修饰静态方法时锁对象变成Class实例public static synchronized void staticMethod() { // 锁对象是ClassName.class }这常被用于实现全局配置的线程安全更新。我曾见过一个坑在Spring Bean的实例方法中使用类锁导致不同实例间产生不必要的竞争。3. 对象头与锁的存储原理3.1 对象内存布局探秘通过JOL工具打印对象头Object obj new Object(); System.out.println(ClassLayout.parseInstance(obj).toPrintable());输出示例64位JVM未开启压缩OFFSET SIZE TYPE DESCRIPTION 0 4 (object header) 01 00 00 00 # Mark Word 4 4 (object header) 00 00 00 00 8 4 (object header) e5 01 00 f8 # Klass Pointer ...Mark Word在不同锁状态下的结构锁状态存储内容标志位无锁哈希码分代年龄01偏向锁线程IDEpoch分代年龄01轻量级锁指向栈中锁记录的指针00重量级锁指向互斥量的指针10GC标记空113.2 偏向锁的优化奥秘偏向锁通过CAS记录线程ID实现无竞争场景的快速锁定。但这两个设计常被忽视批量重偏向Bulk Rebiasing当一类对象的偏向锁被频繁撤销时JVM会为该类所有实例批量重置偏向锁状态偏向锁延迟JDK15后默认4000ms后才启用偏向锁-XX:BiasedLockingStartupDelay实测对比i7-11800H, JDK17// 测试代码循环1000万次同步累加 with biased lock: 238ms without biased lock: 417ms4. 完整的锁升级路径4.1 从无锁到重量级的全过程初始状态对象刚创建时为无锁状态标志位01首次加锁线程通过CAS将Mark Word替换为偏向锁模式存储线程ID出现竞争当其他线程尝试获取时升级为轻量级锁栈帧中创建Lock Record自旋失败超过一定次数-XX:PreBlockSpin控制后膨胀为重量级锁注意锁只能升级不能降级但偏向锁可以被撤销回无锁状态4.2 轻量级锁的二次加锁陷阱当已持有轻量级锁的线程再次加锁时会直接在栈帧中添加Lock Record形成锁重入。但这里有个隐藏坑点synchronized(obj) { // 第一次加锁轻量级锁 synchronized(obj) { // 重入时不检查锁状态 // 如果此时其他线程触发锁升级... } }如果外层锁持有期间其他线程导致锁升级内层解锁时会错误地认为仍是轻量级锁。这是某些JVM版本出现锁泄漏的原因。5. 生产环境中的避坑指南5.1 锁粒度的黄金法则细粒度锁对账户转账场景应该锁定两个账户对象按固定顺序避免死锁public void transfer(Account target, int amount) { Account first this.id target.id ? this : target; Account second this.id target.id ? target : this; synchronized(first) { synchronized(second) { this.balance - amount; target.balance amount; } } }粗粒度锁适用于配置中心的全局参数更新直接使用类锁5.2 死锁检测与排查通过jstack检测死锁jstack -l pid | grep -A10 deadlock典型死锁日志特征Found one Java-level deadlock: Thread-1: waiting to lock monitor 0x00007f88e4003988 (object 0x000000076ab16c58) which is held by Thread-0 Thread-0: waiting to lock monitor 0x00007f88e4003ae8 (object 0x000000076ab16c68) which is held by Thread-15.3 性能优化实战技巧偏向锁调优对明确不存在竞争的代码可通过-XX:-UseBiasedLocking禁用自旋次数调整CPU密集型应用可适当增加PreBlockSpin默认10次逃逸分析辅助配合-XX:DoEscapeAnalysis让对象在栈上分配6. 高频面试题深度剖析6.1 锁升级能否逆向进行不能。锁升级是单向过程主要因为偏向锁撤销需要安全点检查重量级锁涉及操作系统互斥量转换成本高逆向操作带来的收益不如直接使用新锁状态6.2 synchronized与ReentrantLock对比特性synchronizedReentrantLock实现机制JVM内置JDK代码实现锁获取方式自动释放必须显式unlock()条件变量只能配合wait/notify支持多个Condition公平性非公平可配置公平/非公平性能JDK6后优化相当高竞争时略优调试支持有限可获取等待线程信息6.3 对象头在锁状态下的变化过程以64位JVM为例小端存储无锁状态0x00000001: 哈希码(25bit) 分代年龄(4bit) 偏向模式(1bit) 锁标志(2bit)偏向锁状态0x00000501: 线程ID(54bit) Epoch(2bit) 分代年龄(4bit) 偏向模式(1bit) 锁标志(2bit)轻量级锁0xf800c014: 指向Lock Record的指针(62bit) 锁标志(2bit)7. 从字节码看同步实现7.1 同步方法字节码示例编译后的.class文件会设置ACC_SYNCHRONIZED标志public synchronized void demo(); descriptor: ()V flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED7.2 同步代码块字节码分析对应monitorenter/monitorexit指令aload_0 // 加载this引用 dup // 复制栈顶值 astore_1 // 存储引用到局部变量1 monitorenter // 进入监视器 ... // 业务代码 aload_1 // 加载存储的引用 monitorexit // 正常退出监视器 goto 14 aload_1 // 异常处理路径 monitorexit // 确保锁释放 athrow注意编译器会自动生成异常处理块来保证monitorexit执行8. 锁优化的进阶思考8.1 锁消除Lock ElisionJIT编译器通过逃逸分析确定对象不会逃逸出当前线程时会直接移除同步操作public String concat(String s1, String s2) { StringBuffer sb new StringBuffer(); // 局部变量 sb.append(s1); sb.append(s2); return sb.toString(); }在这个例子中StringBuffer的同步操作会被完全消除。8.2 锁粗化Lock Coarsening当检测到连续多个同步块使用同一个锁时JVM会合并这些同步块for(int i0; i100; i) { synchronized(lock) { // 小范围操作 } } // 优化为 synchronized(lock) { for(int i0; i100; i) { // 合并后的操作 } }9. 常见误区与验证实验9.1 synchronized比volatile慢验证代码// 测试volatile volatile int vCounter; void volatileInc() { vCounter; } // 测试synchronized int sCounter; synchronized void syncInc() { sCounter; }测试结果百万次调用volatile: 45ms synchronized(无竞争): 32ms结论单线程场景下synchronized经过优化后性能反而更好。9.2 String作为锁对象的隐患错误示例synchronized(userId.toString()) { // 可能产生相同字符串但不同对象 // 临界区 }正确做法private static final Object LOCK new Object(); synchronized(LOCK) { // 临界区 }10. 从JVM源码看同步实现HotSpot的关键实现简化版对象监视器创建ObjectMonitor* ObjectSynchronizer::inflate(Thread* self, oop object) { // 膨胀为重量级锁时创建ObjectMonitor mark object-mark(); monitor new ObjectMonitor(); monitor-set_header(mark); }锁获取流程void ObjectMonitor::enter(TRAPS) { Thread * const Self THREAD; // 尝试快速获取 if (TryLock(Self) 0) return; // 自旋获取 if (TrySpin(Self) 0) return; // 最终阻塞 EnterI(THREAD); }理解这些底层机制才能真正回答为什么synchronized不会导致线程饿死等问题。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表