synchronized的工作机制是怎样的?深入解析synchronized底层原理

前言

在讲解synchronized原理前,需要先理解几个重要的基本概念:

临界资源:一次只允许一个线程使用的资源

临界区:访问临界资源的代码块

竞态条件:多个线程在临界区执行,由于代码的执行序列不同导致结果无法预测

如何避免竞态条件发生

  • 阻塞式的解决方案:synchronized,lock
  • 非阻塞式的解决方案:原子变量

synchronized

原理
Monitor(监视器)

每个Java对象都可以关联一个monitor对象,Monitor也是Class,其实例存储在堆中,如果使用synchronized给对象上锁(重量级)后,该对象的Mark Word中就被设置指向Monitor对象的指针。

  • Mark Word结构:最后两位是锁标志

工作流程(重量级锁):

  • 开始时 Monitor 中的 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor 中只能有一个 Owner,obj 对象的 Mark Word 指向 Monitor,把对象原有的 MarkWord 存入线程栈中的锁记录

  • 在 Thread-2 上锁的过程,Thread-3、Thread-4、Thread-5 也执行 synchronized(obj),就会进入 EntryList (等待队列),此时线程状态为 BLOCKED阻塞
  • Thread-2 执行完同步代码块的内容,根据 obj 对象头中 Monitor 地址寻找,设置 Owner 为空,把线程栈的锁记录中的对象头的值设置回 MarkWord
  • 当 Owner 为空时,唤醒 EntryList 中等待的线程来竞争锁,竞争是非公平的,没有竞争到的线程继续阻塞

[!CAUTION]

  • synchronized 必须是进入同一个对象的 Monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

锁升级
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁	// 随着竞争的增加,只能锁升级,不能降级

然后,线程使用 CAS 操作尝试将对象头的 Mark Word 替换为指向该锁记录的指针。

偏向锁
  • 目标:消除在无竞争情况下同步原语的性能开销。通常适用于同一个线程多次申请同一个锁的场景。
  • 原理:当第一个线程 T1 访问同步块时,它会通过 CAS 操作将自己的线程ID写入对象的 Mark Word。成功后,这个对象就“偏向了” T1。
  • 后续操作
  • 同一个线程 T1 再次进入同步块时,只需检查 Mark Word 中的线程ID是否是自己。如果是,则无需任何同步操作(如CAS),直接执行,性能极高。
    • 如果有另一个线程 T2 来尝试竞争锁,偏向模式宣告结束
  • 撤销
  • 调用了对象的 hashCode,但偏向锁的对象 Mark Word 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
    • 轻量级锁会在锁记录中记录 hashCode
    • 重量级锁会在 Monitor 中记录 hashCode
  • **批量撤销:**如果对象被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
  • 批量重偏向:当撤销偏向锁阈值超过 20 次后,JVM 会觉得是不是偏向错了,于是在给这些对象加锁时重新偏向至加锁线程
    • 批量撤销:当撤销偏向锁阈值超过 40 次后,JVM 会觉得自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
  • 注意:由于偏向锁的撤销成本较高,在竞争激烈的场景下反而可能降低性能。因此,在 JDK 15 及之后,偏向锁被默认禁用

轻量级锁
  • 目标:在竞争程度很低(“轻度竞争”),即多个线程交替执行 (加锁时间错开) 同步块而非同时竞争的场景下,避免直接使用重量级锁。
  • 加锁原理
  1. 在代码进入同步块时,JVM 会在当前线程的栈帧中创建一个名为锁记录 的空间,用于存储对象当前的 Mark Word 副本。

  1. 然后,线程使用 CAS 操作尝试将对象头的 Mark Word 替换为指向该锁记录的指针。
  • 如果成功,当前线程获得锁。将 Mark Word 的锁标志位变为 00
    • 如果失败,表示至少有一条其他线程(T2)也在竞争锁(即发生了竞争),此时进入锁膨胀过程
    • 如果在这之前,当前线程已经获得了锁,则添加一条 Lock Record 作为重入计数

  1. 解锁:
  • 如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减 1
    • 如果锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头
  • 成功,则解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
  1. 处理竞争:
  • 自旋优化:获得锁失败的线程不会立即被挂起,而是执行一个自选循环,不断尝试获取锁
    • 如果在自旋期间,持有锁的线程释放了锁(通过CAS将Displaced Mark Word写回对象头),那么 就可以成功获得锁,避免了线程挂起的开销。
    • 如果自旋了一定次数后仍然没有获得锁,或者又来了第三个线程竞争,说明竞争加剧,轻量级锁就要膨胀 为重量级锁。

锁膨胀、重量级锁

在尝试加轻量级锁的过程中,CAS 操作无法成功,可能是其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

  • Thread-1 加轻量级锁失败,进入锁膨胀流程:为 Object 对象申请 Monitor 锁,通过 Object 对象头获取到持锁线程,将 Monitor 的 Owner 置为 Thread-0,将 Object 的对象头指向重量级锁地址,然后自己进入 Monitor 的 EntryList BLOCKED

  • 当 Thread-0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头失败,这时进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

对比

优点

缺点

使用场景

偏向锁

加锁解锁不需要额外CAS操作,性能极高

撤销锁的成本高,需要进入安全点

只有一个线程访问同步块

轻量级锁

竞争的线程不会阻塞,通过自旋提高响应速度

长时间自选会消耗CPU

追求响应时间,锁占用时间很短,线程交替执行

重量级锁

竞争激烈时,不自旋,不消耗CPU

线程阻塞,响应时间缓慢,用户态内核态切换开销大

追求吞吐量,锁占用时间较长,竞争激烈

锁优化
自旋锁

重量级锁竞争时,尝试获取锁的线程不会立即阻塞,可以使用自旋(默认 10 次)来进行优化,采用循环的方式去尝试获取锁

注意:

  • 自旋占用 CPU 时间,单核 CPU 自旋就是浪费时间,因为同一时刻只能运行一个线程,多核 CPU 自旋才能发挥优势
  • 自旋失败的线程会进入阻塞状态

锁粗化

如果虚拟机检测到有一连串零碎的操作都对同一个对象反复加锁和解锁,会把加锁同步的范围扩大(粗化) 到整个操作序列的外部,减少不必要的锁请求次数。

锁消除

虚拟机即时编译器(JIT)在运行时,通过逃逸分析技术判断一些代码中不可能存在共享数据竞争,那么就会将这些同步锁消除掉

可见性

JMM 中的 Happens-Before 原则,规定了同一个锁的解锁操作 happens-before 于后续对这个锁的加锁操作,它禁止了指令重排序,并保证了数据同步

底层实现

编译器在生成字节码时,会在 synchronized 同步块的入口和出口处插入相应的内存屏障指令

进入synchronized时:

  • 清空当前线程工作内存(如CPU缓存)中所有关于共享变量的副本
  • 迫使当前线程从主内存重新加载它需要访问的共享变量的最新值
  • 这确保了线程在进入同步块时,能看到之前其他线程释放锁时的最新结果。

退出 synchronized 块时:

  • 它会强制将当前线程工作内存中对共享变量的修改立即刷新回主内存,确保当前线程的修改立刻对其他线程可见

死锁

**四个必要条件:**互斥、请求和保持、不可剥夺、循环等待

定位
  • 使用 jps 定位进程 id,再用 jstack id 定位死锁,找到死锁的线程去查看源码,解决优化
  • Linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 的输出来看各个线程栈
  • 避免死锁:避免死锁要注意加锁顺序
  • 可以使用 jconsole 工具,在 jdk\bin 目录下

活锁

任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直失败、尝试、再失败的循环,例如:两个线程互相改变对方的结束条件,最后谁也无法结束

饥饿

一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值