对synchronized理解&思考

在 Java 并发编程中,synchronized是最基础也最常用的线程安全保障手段。它看似简单,实则背后藏着 JVM 的复杂优化逻辑,且在不同 JDK 版本中经历了多次性能升级。今天我们就从 “实现原理”“版本差异”“工程作用” 三个维度,彻底吃透synchronized

一、synchronized 的底层实现:不止是 “锁”

synchronized的核心是通过 “互斥” 保证临界区代码的原子执行,但其实现依赖 JVM 的对象头监视器锁(Monitor) 机制,不同修饰对象的实现逻辑略有差异。

1. 锁的载体:对象头里的 “锁状态”

Java 中所有对象都有一个 “对象头”(Object Header),其中Mark Word字段存储了对象的锁状态信息,这是synchronized实现的关键。

默认情况下,Mark Word存储对象哈希码、GC 年龄等信息;当对象被加锁时,Mark Word会更新为 “锁指针”,指向对应的锁结构(如偏向锁记录、轻量级锁栈帧)。

2. 两种锁模式的实现逻辑

synchronized根据修饰对象不同,锁的载体也不同:

  • 修饰普通方法 / 代码块:锁载体是 “对象实例”(普通方法锁this,代码块锁自定义对象);

  • 修饰静态方法:锁载体是 “类对象”(如XXX.class),因为静态方法属于类而非实例。

无论哪种模式,最终都会关联到监视器锁(Monitor) ——JVM 中的一种同步机制,每个对象 / 类都对应一个 Monitor。当线程进入synchronized代码块时,会尝试 “获取 Monitor”;执行完后 “释放 Monitor”,其他线程需等待 Monitor 释放才能竞争。

二、JDK 版本差异:从 “重量级锁” 到 “锁升级” 的性能跃迁

synchronized的性能在 JDK 1.6 前后有天壤之别,核心原因是 JDK 1.6 引入了 “锁升级” 机制,通过偏向锁→轻量级锁→重量级锁的渐进式升级,平衡 “线程安全” 与 “性能开销”。

| JDK 版本 | 核心优化 | 锁机制特点 | 适用场景 |

| ----------- | -------------- | -------------------------------------- | -------------- |

| JDK 1.5 及之前 | 无 | 仅支持 “重量级锁”,依赖操作系统内核态互斥量(Mutex),线程切换成本高 | 极少并发场景(性能差) |

| JDK 1.6 及之后 | 锁升级、CAS 算法、自旋锁 | 支持三种锁状态,按需升级,避免频繁内核态切换 | 全场景覆盖(低并发→高并发) |

锁升级的完整流程(JDK 1.6+):

  1. 偏向锁:当只有一个线程反复进入临界区时,JVM 会在Mark Word中记录该线程 ID,后续该线程进入时无需竞争,直接获取锁(几乎无开销);

  2. 轻量级锁:当有第二个线程竞争锁时,偏向锁升级为轻量级锁。线程会通过CAS(Compare and Swap) 尝试修改Mark Word的锁指针,若成功则获取锁,失败则自旋(循环重试);

  3. 重量级锁:当自旋次数超过阈值(默认 10 次),或更多线程参与竞争时,轻量级锁升级为重量级锁。此时线程会阻塞并进入内核态等待,由操作系统调度唤醒(开销最高,但适合高并发)。

这种 “能不加锁就不加锁,能轻锁就不轻转重” 的设计,让synchronized在低并发场景下性能接近无锁,高并发场景下保证安全。

三、工程开发中的核心作用:解决并发 “痛点”

在实际项目中,synchronized是应对线程安全问题的 “基础工具”,主要解决以下三类核心场景:

1. 保证原子操作:避免 “中间态” 数据错乱

当多个线程执行 “多步操作”(如 “判断库存→扣减库存”“读取计数→累加计数”)时,synchronized能保证这些操作成为 “不可分割的原子块”,避免其他线程在中间步骤打断。

工程实例:电商秒杀中的库存扣减,用synchronized包裹 “判断库存是否大于 0→扣减库存” 的逻辑,防止超卖。

2. 保证可见性:避免 “缓存不一致”

线程修改共享变量后,若未及时刷新到主内存,其他线程可能读取到旧值(缓存不一致)。synchronized在释放锁时会强制刷新缓存,让修改后的变量同步到主内存;获取锁时会清空本地缓存,从主内存重新读取变量,从而保证可见性。

工程实例:多线程控制的 “服务开关”,用synchronized修饰开关的修改与读取逻辑,确保线程能立即感知开关状态变化。

3. 保证有序性:避免 “指令重排序”

JVM 为优化性能会重排序指令,可能导致代码执行顺序与预期不一致(如单例模式中 “对象未初始化完成就赋值给引用”)。synchronized会禁止临界区内的指令重排序,同时保证 “一个线程释放锁后,另一个线程获取锁前” 的操作不会交叉,从而保证有序性。

工程实例:非 volatile 修饰的单例模式,用synchronized修饰实例创建逻辑,避免指令重排序导致的空指针异常。

四、工程使用的 “避坑” 建议

  1. 缩小锁粒度:尽量修饰代码块而非整个方法,只锁定 “真正需要同步的逻辑”(如库存扣减只锁扣减部分,而非整个商品查询方法),减少线程阻塞时间;

  2. 避免锁对象变化:锁对象(如this、自定义对象)不能是 “可变对象”(如用 String 作为锁对象,可能因字符串常量池复用导致锁冲突),建议用private final Object lock = new Object();定义不可变锁对象;

  3. 不嵌套锁:避免在synchronized代码块中嵌套另一个synchronized(如 “锁 A” 中调用 “锁 B” 的同步方法),防止死锁;

  4. 高并发场景慎用:若并发量极高(如每秒万级请求),synchronized的重量级锁可能成为性能瓶颈,此时可结合 “并发容器(如 ConcurrentHashMap)”“原子类(如 AtomicInteger)” 等更轻量的方案优化。

五、总结

synchronized从 JDK 1.5 的 “重量级锁” 进化到 JDK 1.6 + 的 “智能锁升级”,早已不是 “性能杀手”,而是工程开发中 “简单可靠” 的并发解决方案。它的核心价值在于用较低的理解成本,解决原子性、可见性、有序性三大并发痛点,是 Java 开发者必须掌握的基础工具。

在实际项目中,无需盲目追求复杂的并发框架,先学好synchronized的底层逻辑与使用场景,才能在 “安全” 与 “性能” 之间找到平衡,写出更稳定的并发代码。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容