synchronized 底层原理

synchronized 底层使用 Monitor 指令保证原子性
Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性;

Monitor工作原理

Monitor 被翻译为监视器或者管程;
JVM 通过通知进入和退出 monitor 对象实现对方法和同步块的同步。
在进入同步方法之前加入 monitor enter,在退出方法和异常处加入 monitor exit,JVM保证每个 monitor enter 必须有对应的 monitor exit与之匹配。每个 Java 对象都可以关联一个 monitor 对象,当且仅有一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitor enter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。

Monitor 结构

image.png

Monitor 指令

  • 获取锁:monitor enter
  • 释放锁:monitor exit

指令会有两个 monitor exit ,防止指令异常

锁升级的总过程

无锁——>偏向锁——>轻量级锁——>(自旋锁、自适应锁)——>重量级锁

锁标志位

synchronized和锁升级 - 图2

使用

  1. 静态方法:锁的是对象的 Class 实例;
  2. 非静态方法:锁的是当前对象的实例;
  3. 作用于某个对象(代码块):锁的是括号括起来的对象实例;

    偏向锁

    一个对象创建时
  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后3位为101,此时它的 thread、apoch、age都为0。
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以在 JVM 参数添加 -XX:BiasedLockingStartupDelay=0 来禁止延迟加载。
  • 如果没有开启偏向锁,那么对象创建后, markword 值为 0x01 即最后3位为 001,这时它的 hashcode、age 都为0,第一次用到 hashcode 时才会赋值。

注意:偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完成同步代码,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁;

偏向锁上锁步骤

  1. 检测 markword 是否为可偏向状态,即为是否为偏向锁1,锁标志位为01;
    1. 若为可偏向状态,则检测线程 id 是否为当前线程 id,如果是,则执行同步代码块。
      1. 否则通过 CAS 操作竞争锁,竞争成功,则将 markword 的线程 id 替换为当前线程 id,执行同步代码块;
      2. 如果通过 CAS 操作竞争所失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续执行同步代码块;

        偏向锁的作用

        为了在没有多线程竞争的情况下尽量减少不必要的轻量级锁的执行。因为轻量级锁的加锁解锁操作时需要依赖多次CAS原子指令的,而偏向锁只需要在置换 ThreadId 的时候依赖一次 CAS 原子指令(由于一旦出现多线程的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗也必须要小于节省下来的CAS原子指令的性能消耗)

        撤销偏向锁

        调用对象的 hashCode ,但偏向锁的对象 markword 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
  • 轻量级锁会在锁记录中记录 hashCode
  • 重量级锁会在 Monitor 中记录 hashCode
  • 调用 wait、notify会撤销偏向锁

在调用 HashCode 后使用偏向锁,需要去掉 -xx:-UseBiasedLocking

批量重偏向

如果对象虽然被多格线程访问,但没有竞争,这是偏向了线程 t1 的对象仍有机会重新偏向 t2,重偏向会重置对象的 ThreadID;
当撤销偏向锁阈值超过默认值( 20 次)后, jvm 会觉得,是不是偏向错了,于是会把这些对象加锁时重新偏向至加锁线程;

批量撤销

当撤销偏向锁阈值超过40次后,jvm 会觉得,自己确实偏向错了,根本就不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的;

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有很多线程访问,但是多线程访问的时间是错开的(也可以说是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍是 synchronized 关键字;

  1. 在进入同步代码块的时候,虚拟机首先在当前线程创建锁记录(Lock Record)对象,用于存储锁对象目前的 mark word 的拷贝,官方称之为 Displaced Mark Word;

image.png

  1. 让锁记录(Lock Record)中 Object reference 指定锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录中;

image.png

  1. 如果 cas 替换成功,虚拟机尝试将对象 mark word 中的 lock word 更新为指向当前线程的lock record的指针,并将lock record 里的 owner 指针指向 object mark word。如果成功则表示拥有了该对象的锁,并且对象 mark word 的锁标志位设置为 00,即表示此对象处于轻量级锁定状态;

image.png

  1. 如果 cas 替换失败,由两种情况
    • 如果是其他线程已经持有了该 Object 的轻量级锁,这是表明有竞争,进入锁膨胀过程
    • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

image.png

  1. 当退出 synchronized 代码块(解锁),如果有取值为null的锁记录,表示有重入,这是重置锁记录,表示重入计数减一。当退出 synchronized 代码块(解锁时)锁记录的值没有null,这时使用 cas 将 Mark Word 的值恢复给对象头
    • 成功,解锁成功
    • 失败,说明轻量级锁进行了锁膨胀或者已经升级为重量级锁,进入重量级锁解锁过程

      自旋锁

      一个线程尝试获取某个锁时,如果改锁已经被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或者睡眠状态,叫做自旋锁;
      线程自旋到默认值后(默认值为20次),会被线程挂起;

      自适应自选锁

      所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定的;

      锁膨胀(锁升级)

      当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
      image.png
      这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
  • 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
  • 然后自己进入 Monitor 的 EntryList BLOCKED

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

锁消除

锁消除是Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

  1. # 开始锁消除(默认开启)
  2. -XX:+EliminateLocks
  3. # 关闭锁消除
  4. -XX:-EliminateLocks

锁粗化

JIt 编译器在执行动态编译时,若发现前后相邻的 synchronized 块使用的是同一个锁对象,那么它就会把这几个synchronized 块合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就不用频繁的申请和释放锁了,从而达到申请与释放锁一次,就可以执行完全部的同步代码块,从而提升了性能。