synchronized 底层原理
synchronized 底层使用 Monitor 指令保证原子性
Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性;
Monitor工作原理
Monitor 被翻译为监视器或者管程;
JVM 通过通知进入和退出 monitor 对象实现对方法和同步块的同步。
在进入同步方法之前加入 monitor enter,在退出方法和异常处加入 monitor exit,JVM保证每个 monitor enter 必须有对应的 monitor exit与之匹配。每个 Java 对象都可以关联一个 monitor 对象,当且仅有一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitor enter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
Monitor 结构
Monitor 指令
- 获取锁:monitor enter
- 释放锁:monitor exit
锁升级的总过程
无锁——>偏向锁——>轻量级锁——>(自旋锁、自适应锁)——>重量级锁
锁标志位
使用
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后3位为101,此时它的 thread、apoch、age都为0。
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以在 JVM 参数添加
-XX:BiasedLockingStartupDelay=0
来禁止延迟加载。 - 如果没有开启偏向锁,那么对象创建后, markword 值为 0x01 即最后3位为 001,这时它的 hashcode、age 都为0,第一次用到 hashcode 时才会赋值。
注意:偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完成同步代码,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁;
偏向锁上锁步骤
- 检测 markword 是否为可偏向状态,即为是否为偏向锁1,锁标志位为01;
- 若为可偏向状态,则检测线程 id 是否为当前线程 id,如果是,则执行同步代码块。
- 否则通过 CAS 操作竞争锁,竞争成功,则将 markword 的线程 id 替换为当前线程 id,执行同步代码块;
- 如果通过 CAS 操作竞争所失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续执行同步代码块;
偏向锁的作用
为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁的执行。因为轻量级锁的加锁解锁操作时需要依赖多次CAS原子指令的,而偏向锁只需要在置换 ThreadId 的时候依赖一次 CAS 原子指令(由于一旦出现多线程的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗也必须要小于节省下来的CAS原子指令的性能消耗)撤销偏向锁
调用对象的 hashCode ,但偏向锁的对象 markword 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
- 若为可偏向状态,则检测线程 id 是否为当前线程 id,如果是,则执行同步代码块。
- 轻量级锁会在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
- 调用 wait、notify会撤销偏向锁
在调用 HashCode 后使用偏向锁,需要去掉 -xx:-UseBiasedLocking
批量重偏向
如果对象虽然被多格线程访问,但没有竞争,这是偏向了线程 t1 的对象仍有机会重新偏向 t2,重偏向会重置对象的 ThreadID;
当撤销偏向锁阈值超过默认值( 20 次)后, jvm 会觉得,是不是偏向错了,于是会把这些对象加锁时重新偏向至加锁线程;
批量撤销
当撤销偏向锁阈值超过40次后,jvm 会觉得,自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的;
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有很多线程访问,但是多线程访问的时间是错开的(也可以说是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍是 synchronized
关键字;
- 在进入同步代码块的时候,虚拟机首先在当前线程创建锁记录(Lock Record)对象,用于存储锁对象目前的 mark word 的拷贝,官方称之为 Displaced Mark Word;
- 让锁记录(Lock Record)中
Object reference
指定锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录中;
- 如果 cas 替换成功,虚拟机尝试将对象 mark word 中的 lock word 更新为指向当前线程的lock record的指针,并将lock record 里的 owner 指针指向 object mark word。如果成功则表示拥有了该对象的锁,并且对象 mark word 的锁标志位设置为 00,即表示此对象处于轻量级锁定状态;
- 如果 cas 替换失败,由两种情况
- 如果是其他线程已经持有了该 Object 的轻量级锁,这是表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 当退出
synchronized
代码块(解锁),如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入计数减一。当退出synchronized
代码块(解锁时)锁记录的值没有null,这时使用 cas 将 Mark Word 的值恢复给对象头
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
当Thread-0 退出同步代码块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,如果失败,此时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
锁消除
锁消除是Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
# 开始锁消除(默认开启)
-XX:+EliminateLocks
# 关闭锁消除
-XX:-EliminateLocks
锁粗化
JIt 编译器在执行动态编译时,若发现前后相邻的 synchronized 块使用的是同一个锁对象,那么它就会把这几个synchronized 块合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就不用频繁的申请和释放锁了,从而达到申请与释放锁一次,就可以执行完全部的同步代码块,从而提升了性能。