synchronized 原理

简介: `synchronized` 是 Java 中实现线程同步的关键字,通过对象头中的 Monitor 和锁机制确保同一时间只有一个线程执行同步代码。其底层依赖 Mark Word 和 Monitor 控制锁状态,支持偏向锁、轻量级锁和重量级锁的升级过程,以优化性能。同步方法和同步块在实现方式上有所不同,前者通过 `ACC_SYNCHRONIZED` 标志隐式加锁,后者通过 `monitorenter` 和 `monitorexit` 指令显式控制。此外,`synchronized` 还保证内存可见性和 Happens-Before 关系,使共享变量在多线程间正确同步。

synchronized 是 Java 中用于实现线程同步的关键字,确保同一时刻只有一个线程可以执行被修饰的代码块或方法。其核心原理基于 Java 对象头中的监视器(Monitor)锁机制,下面从底层实现、锁升级、内存语义三个层面详细解析:

1. 底层实现:Monitor 与对象头

Monitor(监视器)

  • 本质:每个 Java 对象都可以关联一个 Monitor,它是实现同步的基础,底层由 C++ 的 ObjectMonitor 类实现。
  • 作用:Monitor 是一个同步工具,相当于一个许可证,线程只有获取到 Monitor 才能执行同步代码,执行完毕后释放 Monitor。

对象头(Object Header)

  • Java 对象在内存中的布局分为三部分:对象头、实例数据、对齐填充。
  • Mark Word:对象头的一部分,用于存储对象的哈希码、分代年龄和锁状态信息。锁状态不同时,Mark Word 的存储内容会发生变化:
    • 无锁:存储对象的哈希码和分代年龄。
    • 轻量级锁:存储指向线程栈中锁记录的指针。
    • 重量级锁:存储指向 Monitor 的指针。

2. 锁的升级过程

JDK 6 之后,synchronized 进行了大量优化,引入了偏向锁、轻量级锁、重量级锁的升级机制,以减少线程竞争带来的性能损耗。锁的升级过程是不可逆的:

偏向锁(Biased Locking)

  • 适用场景:只有一个线程访问同步块的情况。
  • 原理
    1. 当第一个线程访问同步块并获取锁时,会在 Mark Word 中记录该线程的 ID(偏向线程 ID)。
    2. 后续该线程再次进入同步块时,无需任何同步操作,直接获取锁,效率极高。
  • 撤销条件:当有其他线程尝试竞争偏向锁时,持有偏向锁的线程会被挂起,偏向锁升级为轻量级锁。

轻量级锁(Lightweight Lock)

  • 适用场景:多个线程交替访问同步块,无实际竞争。
  • 原理
    1. 线程进入同步块前,JVM 会在当前线程的栈帧中创建一个锁记录(Lock Record)。
    2. 通过 CAS(Compare and Swap)操作尝试将 Mark Word 更新为指向锁记录的指针:
      • 成功:获取轻量级锁。
      • 失败:表示有其他线程竞争,锁升级为重量级锁。
  • 释放锁:通过 CAS 将 Mark Word 恢复为无锁状态。

重量级锁(Heavyweight Lock)

  • 适用场景:多个线程同时竞争锁。
  • 原理
    1. 锁升级为重量级锁后,Mark Word 存储指向 Monitor 的指针。
    2. 未获取到锁的线程会被阻塞(进入 Monitor 的 EntryList),释放 CPU 资源。
    3. 锁释放时,会唤醒 EntryList 中的线程重新竞争。
  • 缺点:线程阻塞和唤醒涉及用户态与内核态的切换,性能开销大。

3. 同步方法与同步块的实现差异

同步方法(synchronized method

  • 隐式实现:通过方法修饰符 synchronized 实现。
  • 原理:JVM 通过方法表中的 ACC_SYNCHRONIZED 标志来识别同步方法。调用时,JVM 会自动检查该标志,获取当前对象或类的 Monitor 后执行方法。

同步块(synchronized(this)

  • 显式实现:通过 synchronized 代码块实现。
  • 原理
    1. 进入同步块前,执行 monitorenter 指令,尝试获取 Monitor。
    2. 退出同步块时,执行 monitorexit 指令(正常退出和异常退出各有一个),释放 Monitor。

4. 内存语义与 Happens-Before 规则

synchronized 不仅保证了线程互斥,还具有内存可见性的语义:

  • 解锁(释放 Monitor):JVM 会将当前线程的本地内存中的共享变量刷新到主内存。
  • 加锁(获取 Monitor):JVM 会将主内存中的共享变量最新值更新到当前线程的本地内存。
  • Happens-Before 规则:对一个 Monitor 的解锁操作 Happens-Before 后续对同一个 Monitor 的加锁操作,确保了可见性。

5. 性能优化建议

  • 减少锁的粒度:避免对整个方法加锁,优先使用同步块。
  • 降低锁的持有时间:将不需要同步的代码移出同步块。
  • 优先使用偏向锁/轻量级锁场景:对于单线程或交替执行的同步代码,性能提升显著。
  • 考虑替代方案:高并发场景下,可使用 ReentrantLockConcurrentHashMap 等更灵活的同步工具。

总结

synchronized 的核心原理是基于 Monitor 和对象头的锁机制,通过锁升级策略(偏向锁 → 轻量级锁 → 重量级锁)在不同场景下平衡性能和线程安全。理解其底层实现有助于写出更高效、更安全的多线程代码。

目录
相关文章
|
存储 Java
ArrayList的初始化容量与扩容机制解析
ArrayList的初始化容量与扩容机制解析
|
存储 监控 安全
吃透synchronized实现原理
吃透synchronized实现原理
343 0
|
消息中间件 存储 RocketMQ
Rocketmq如何保证消息不丢失
文章分析了RocketMQ如何通过生产者端的同步发送与重试机制、Broker端的持久化存储与消息重试投递策略、以及消费者端的手动提交ack与幂等性处理,来确保消息在整个传输和消费过程中的不丢失。
|
12月前
|
缓存 Java
对比 synchronized 和 volatile
`synchronized` 和 `volatile` 是 Java 并发编程中的两个关键机制,各有侧重。`synchronized` 用于实现线程的互斥访问,保证原子性、可见性和有序性,适用于需要锁的场景;而 `volatile` 更轻量,仅确保变量的可见性和有序性,适用于状态标志等无需复合操作的场景。两者可互补使用,如双重检查单例中结合二者优势。合理选择有助于提升并发性能与代码安全性。
525 0
|
12月前
|
存储 安全 Java
synchronized 锁升级
JDK 6 引入的 synchronized 锁升级机制,通过偏向锁、轻量级锁和重量级锁的动态切换,优化了多线程同步性能。该机制根据竞争情况逐步升级锁状态,减少线程阻塞和系统调用开销,从而提升并发效率。
531 0
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
消息中间件 架构师 Java
美团面试:对比分析 RocketMQ、Kafka、RabbitMQ 三大MQ常见问题?
美团面试:对比分析 RocketMQ、Kafka、RabbitMQ 三大MQ常见问题?
美团面试:对比分析 RocketMQ、Kafka、RabbitMQ 三大MQ常见问题?
|
11月前
|
安全 Java C++
synchronized 原理
本文详解 Java 中 `synchronized` 的底层实现原理及锁升级机制。通过 Monitor 对象管理线程竞争,涉及 owner、EntryList、WaitSet 等结构,并介绍偏向锁、轻量级锁、重量级锁的升级过程。同时对比 `synchronized` 与 `volatile`、`Lock` 的区别,涵盖原子性、可见性、有序性及功能扩展性,帮助理解并发编程中的线程安全机制。
308 0
|
存储 安全 Java
ThreadLocal - 原理与应用场景详解
ThreadLocal是Java中用于实现线程隔离的重要工具,为每个线程提供独立的变量副本,避免多线程数据共享带来的安全问题。其核心原理是通过 ThreadLocalMap 实现键值对存储,每个线程维护自己的存储空间。ThreadLocal 广泛应用于线程隔离、跨层数据传递、复杂调用链路的全局参数传递及数据库连接管理等场景。此外,InheritableThreadLocal 支持子线程继承父线程的变量值,而 TransmittableThreadLocal 则解决了线程池中变量传递的问题,提升了多线程上下文管理的可靠性。深入理解这些机制,有助于开发者更好地解决多线程环境下的数据隔离与共享挑战。
2226 44